Files
Jonathan Miller 1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
fix: standardize file headers — REPO rename, SPDX case, missing fields
- 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>
2026-05-11 17:01:17 -05:00

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";
}