1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Update REPO: from MokoStandards-API to moko-platform in 125 files - Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files - Fix SPDX-LICENSE-IDENTIFIER case in 2 template files - Add missing REPO: field to 3 files Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
228 lines
8.2 KiB
PHP
228 lines
8.2 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: MokoStandards.Maintenance
|
|
* INGROUP: MokoStandards
|
|
* 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
|
|
*
|
|
* USAGE
|
|
* php maintenance/repo_inventory.php # Generate and post dashboard
|
|
* php maintenance/repo_inventory.php --dry-run # Preview only
|
|
* php maintenance/repo_inventory.php --json # JSON output to stdout
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
$dryRun = in_array('--dry-run', $argv);
|
|
$jsonOut = in_array('--json', $argv);
|
|
|
|
$org = 'mokoconsulting-tech';
|
|
foreach ($argv as $i => $arg) {
|
|
if ($arg === '--org' && isset($argv[$i + 1])) { $org = $argv[$i + 1]; }
|
|
}
|
|
|
|
$config = \MokoEnterprise\Config::load();
|
|
try {
|
|
$_adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
|
$_api = $_adapter->getApiClient();
|
|
} catch (\Exception $e) {
|
|
fwrite(STDERR, "Platform init failed: " . $e->getMessage() . "\n");
|
|
exit(1);
|
|
}
|
|
$token = $config->getString('platform', 'gitea') === 'gitea'
|
|
? $config->getString('gitea.token', '')
|
|
: $config->getString('github.token', '');
|
|
|
|
$ALWAYS_EXCLUDE = ['MokoStandards', '.github-private'];
|
|
|
|
/**
|
|
* @return array{int, array<string, mixed>}
|
|
*/
|
|
function ghApi(string $method, string $path, ?array $body, string $token): array
|
|
{
|
|
global $_api;
|
|
try {
|
|
$result = match ($method) {
|
|
'GET' => $_api->get("/{$path}"),
|
|
'POST' => $_api->post("/{$path}", $body ?? []),
|
|
'PATCH' => $_api->patch("/{$path}", $body ?? []),
|
|
'PUT' => $_api->put("/{$path}", $body ?? []),
|
|
'DELETE' => $_api->delete("/{$path}"),
|
|
default => throw new \RuntimeException("Unsupported method: {$method}"),
|
|
};
|
|
return [200, $result];
|
|
} catch (\Exception $e) {
|
|
return [500, ['message' => $e->getMessage()]];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GitHub GraphQL helper.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
function graphql(string $query, array $variables, string $token): array
|
|
{
|
|
global $config;
|
|
$pf = isset($config) ? $config->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 ' . $token,
|
|
'Content-Type: application/json',
|
|
'User-Agent: MokoStandards-Inventory',
|
|
],
|
|
]);
|
|
$body = (string) curl_exec($ch);
|
|
curl_close($ch);
|
|
return json_decode($body, true)['data'] ?? [];
|
|
}
|
|
|
|
// ── Fetch all repos ─────────────────────────────────────────────────────
|
|
if (!$jsonOut) { echo "Fetching repositories from {$org}...\n"; }
|
|
|
|
$allRepos = [];
|
|
$page = 1;
|
|
do {
|
|
[$_, $batch] = ghApi('GET', "orgs/{$org}/repos?per_page=100&page={$page}&type=all&sort=full_name", null, $token);
|
|
$allRepos = array_merge($allRepos, $batch);
|
|
$page++;
|
|
} while (count($batch) === 100);
|
|
|
|
if (!$jsonOut) { echo "Found " . count($allRepos) . " total repositories\n\n"; }
|
|
|
|
// ── Build inventory ─────────────────────────────────────────────────────
|
|
$inventory = [];
|
|
|
|
foreach ($allRepos as $repo) {
|
|
$name = $repo['name'];
|
|
if (in_array($name, $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;
|
|
}
|
|
|
|
// Detect platform from .mokostandards
|
|
foreach (['.github/.mokostandards', '.mokostandards'] as $path) {
|
|
[$status, $data] = ghApi('GET', "repos/{$org}/{$name}/contents/{$path}", null, $token);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Check rulesets count
|
|
[$status, $rulesets] = ghApi('GET', "repos/{$org}/{$name}/rulesets?per_page=100&includes_parents=true", null, $token);
|
|
if ($status === 200 && is_array($rulesets)) {
|
|
$entry['rulesets'] = count($rulesets);
|
|
}
|
|
|
|
// Check for GitHub Project
|
|
$gql = graphql(
|
|
'query($owner:String!,$name:String!){repository(owner:$owner,name:$name){projectsV2(first:1){totalCount}}}',
|
|
['owner' => $org, 'name' => $name],
|
|
$token
|
|
);
|
|
$entry['has_project'] = ($gql['repository']['projectsV2']['totalCount'] ?? 0) > 0;
|
|
|
|
$inventory[] = $entry;
|
|
|
|
if (!$jsonOut) {
|
|
echo " {$name}: {$entry['platform']} | v{$entry['version']} | rulesets:{$entry['rulesets']} | project:" . ($entry['has_project'] ? 'yes' : 'no') . "\n";
|
|
}
|
|
}
|
|
|
|
// ── JSON output ─────────────────────────────────────────────────────────
|
|
if ($jsonOut) {
|
|
echo json_encode($inventory, JSON_PRETTY_PRINT) . "\n";
|
|
exit(0);
|
|
}
|
|
|
|
// ── Build dashboard ─────────────────────────────────────────────────────
|
|
$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";
|
|
$body .= "**Organisation:** `{$org}`\n";
|
|
$body .= "**Generated:** {$now}\n";
|
|
$body .= "**Active:** {$activeN} | **Archived:** {$archivedN} | **Rulesets 3/3:** {$withRules} | **Projects:** {$withProj}\n\n";
|
|
$body .= "| Repository | Visibility | Platform | Version | Rulesets | Project | Issues |\n";
|
|
$body .= "|---|---|---|---|---|---|---|\n";
|
|
$body .= $table . "\n\n";
|
|
$body .= "---\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";
|
|
|
|
// ── Post as issue ───────────────────────────────────────────────────────
|
|
if (!$dryRun) {
|
|
$title = "dashboard: repository inventory ({$org})";
|
|
[$_, $existing] = ghApi('GET', "repos/{$org}/MokoStandards/issues?labels=inventory&state=all&per_page=1&sort=created&direction=desc", null, $token);
|
|
|
|
if (!empty($existing[0]['number'])) {
|
|
$num = $existing[0]['number'];
|
|
ghApi('PATCH', "repos/{$org}/MokoStandards/issues/{$num}", [
|
|
'title' => $title, 'body' => $body, 'state' => 'open', 'assignees' => ['jmiller'],
|
|
], $token);
|
|
echo "Updated inventory issue #{$num}\n";
|
|
} else {
|
|
[$_, $issue] = ghApi('POST', "repos/{$org}/MokoStandards/issues", [
|
|
'title' => $title, 'body' => $body,
|
|
'labels' => ['inventory', 'type: chore', 'automation'],
|
|
'assignees' => ['jmiller'],
|
|
], $token);
|
|
echo "Created inventory issue #{$issue['number']}\n";
|
|
}
|
|
} else {
|
|
echo "(dry-run) would post inventory dashboard issue\n";
|
|
}
|