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
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>
240 lines
9.6 KiB
PHP
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());
|