Files
Jonathan Miller 66e728b078
Generic: Repo Health / Access control (push) Successful in 18s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 27s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 28s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m7s
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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (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
style: fix PHPCS violations across migrated CLI scripts
Auto-fixed 5006 tab-indent and line-ending errors via phpcbf, then
manually broke 100 lines exceeding 150-char limit. All 74 files in
cli/, automation/, maintenance/, deploy/ now pass PHPCS PSR-12 clean.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:36:05 -05:00

240 lines
9.6 KiB
PHP

#!/usr/bin/env 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: MokoPlatform.Maintenance
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /maintenance/repo_inventory.php
* BRIEF: Generate a live inventory dashboard of all governed repos as a GitHub issue
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
class RepoInventoryCli extends CliFramework
{
private $api = null;
private string $token = '';
private $platformConfig = null;
private const ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
protected function configure(): void
{
$this->setDescription('Generate a live inventory dashboard of all governed repos');
$this->addArgument('--org', 'Organization', 'mokoconsulting-tech');
$this->addArgument('--json', 'JSON output to stdout', false);
}
protected function initialize(): void
{
$this->platformConfig = \MokoEnterprise\Config::load();
try {
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($this->platformConfig);
$this->api = $adapter->getApiClient();
} catch (\Exception $e) {
$this->log('ERROR', "Platform init failed: " . $e->getMessage());
exit(1);
}
$this->token = $this->platformConfig->getString('platform', 'gitea') === 'gitea'
? $this->platformConfig->getString('gitea.token', '') : $this->platformConfig->getString('github.token', '');
}
protected function run(): int
{
$org = $this->getArgument('--org');
$jsonOut = (bool) $this->getArgument('--json');
if (!$jsonOut) {
echo "Fetching repositories from {$org}...\n";
}
$allRepos = [];
$page = 1;
do {
[$_, $batch] = $this->ghApi('GET', "orgs/{$org}/repos?per_page=100&page={$page}&type=all&sort=full_name", null);
$allRepos = array_merge($allRepos, $batch);
$page++;
} while (count($batch) === 100);
if (!$jsonOut) {
echo "Found " . count($allRepos) . " total repositories\n\n";
}
$inventory = [];
foreach ($allRepos as $repo) {
$name = $repo['name'];
if (in_array($name, self::ALWAYS_EXCLUDE, true)) {
continue;
}
$entry = [
'name' => $name,
'visibility' => $repo['private'] ? 'private' : 'public',
'archived' => $repo['archived'] ?? false,
'platform' => '-',
'version' => '-',
'last_push' => $repo['pushed_at'] ?? '-',
'open_issues' => $repo['open_issues_count'] ?? 0,
'has_project' => false,
'rulesets' => 0,
];
if ($entry['archived']) {
$inventory[] = $entry;
continue;
}
foreach (['.github/.mokostandards', '.mokostandards'] as $path) {
[$status, $data] = $this->ghApi('GET', "repos/{$org}/{$name}/contents/{$path}", null);
if ($status === 200 && !empty($data['content'])) {
$content = base64_decode($data['content']);
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
$entry['platform'] = trim($m[1], " \t\n\r\"'");
}
if (preg_match('/^version:\s*(.+)/m', $content, $m)) {
$entry['version'] = trim($m[1], " \t\n\r\"'");
}
break;
}
}
[$status, $rulesets] = $this->ghApi('GET', "repos/{$org}/{$name}/rulesets?per_page=100&includes_parents=true", null);
if ($status === 200 && is_array($rulesets)) {
$entry['rulesets'] = count($rulesets);
}
$gql = $this->graphql(
'query($owner:String!,$name:String!)'
. '{repository(owner:$owner,name:$name)'
. '{projectsV2(first:1){totalCount}}}',
['owner' => $org, 'name' => $name]
);
$entry['has_project'] = ($gql['repository']['projectsV2']['totalCount'] ?? 0) > 0;
$inventory[] = $entry;
if (!$jsonOut) {
$proj = $entry['has_project'] ? 'yes' : 'no';
echo " {$name}: {$entry['platform']}"
. " | v{$entry['version']}"
. " | rulesets:{$entry['rulesets']}"
. " | project:{$proj}\n";
}
}
if ($jsonOut) {
echo json_encode($inventory, JSON_PRETTY_PRINT) . "\n";
return 0;
}
$now = gmdate('Y-m-d H:i:s') . ' UTC';
$active = array_filter($inventory, fn($r) => !$r['archived']);
$archived = array_filter($inventory, fn($r) => $r['archived']);
$withRules = count(array_filter($active, fn($r) => $r['rulesets'] >= 3));
$withProj = count(array_filter($active, fn($r) => $r['has_project']));
$activeN = count($active);
$archivedN = count($archived);
$rows = [];
foreach ($inventory as $r) {
$vis = $r['visibility'] === 'private' ? 'prv' : 'pub';
$arch = $r['archived'] ? ' archived' : '';
$proj = $r['has_project'] ? 'yes' : '-';
$rs = $r['archived'] ? '-' : ($r['rulesets'] >= 3 ? '3/3' : "{$r['rulesets']}/3");
$rows[] = "| `{$r['name']}` | {$vis}{$arch} | {$r['platform']} | {$r['version']} | {$rs} | {$proj} | {$r['open_issues']} |";
}
$table = implode("\n", $rows);
$body = "## Repository Inventory Dashboard\n\n"
. "**Organisation:** `{$org}`\n"
. "**Generated:** {$now}\n"
. "**Active:** {$activeN} | **Archived:** {$archivedN}"
. " | **Rulesets 3/3:** {$withRules}"
. " | **Projects:** {$withProj}\n\n"
. "| Repository | Visibility | Platform"
. " | Version | Rulesets | Project | Issues |\n"
. "|---|---|---|---|---|---|---|\n"
. "{$table}\n\n---\n"
. "*Auto-generated by `repo_inventory.php`*\n";
echo "\n" . str_repeat('-', 50) . "\n";
echo "Active: {$activeN} | Archived: {$archivedN}"
. " | Rulesets 3/3: {$withRules}"
. " | Projects: {$withProj}\n";
if (!$this->dryRun) {
$title = "dashboard: repository inventory ({$org})";
$issueQuery = "repos/{$org}/moko-platform/issues"
. "?labels=inventory&state=all&per_page=1"
. "&sort=created&direction=desc";
[$_, $existing] = $this->ghApi('GET', $issueQuery, null);
if (!empty($existing[0]['number'])) {
$num = $existing[0]['number'];
$this->ghApi(
'PATCH',
"repos/{$org}/moko-platform/issues/{$num}",
[
'title' => $title,
'body' => $body,
'state' => 'open',
'assignees' => ['jmiller'],
]
);
echo "Updated inventory issue #{$num}\n";
} else {
[$_, $issue] = $this->ghApi(
'POST',
"repos/{$org}/moko-platform/issues",
[
'title' => $title,
'body' => $body,
'labels' => ['inventory', 'type: chore', 'automation'],
'assignees' => ['jmiller'],
]
);
echo "Created inventory issue #{$issue['number']}\n";
}
} else {
echo "(dry-run) would post inventory dashboard issue\n";
}
return 0;
}
private function ghApi(string $method, string $path, ?array $body): array
{
try {
$result = match ($method) {
'GET' => $this->api->get("/{$path}"), 'POST' => $this->api->post("/{$path}", $body ?? []),
'PATCH' => $this->api->patch("/{$path}", $body ?? []), 'PUT' => $this->api->put("/{$path}", $body ?? []),
'DELETE' => $this->api->delete("/{$path}"), default => throw new \RuntimeException("Unsupported: {$method}"),
};
return [200, $result];
} catch (\Exception $e) {
return [500, ['message' => $e->getMessage()]];
}
}
private function graphql(string $query, array $variables): array
{
$pf = $this->platformConfig !== null ? $this->platformConfig->getString('platform', 'gitea') : 'gitea';
if ($pf !== 'github') {
return [];
}
$payload = json_encode(['query' => $query, 'variables' => $variables]);
$ch = curl_init('https://api.github.com/graphql');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: bearer ' . $this->token,
'Content-Type: application/json',
'User-Agent: moko-platform-Inventory',
],
]);
$body = (string) curl_exec($ch);
curl_close($ch);
return json_decode($body, true)['data'] ?? [];
}
}
$app = new RepoInventoryCli();
exit($app->execute());