Files
moko-platform/maintenance/update_repo_inventory.php
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

311 lines
11 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.Scripts.Maintenance
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /maintenance/update_repo_inventory.php
* BRIEF: Queries GitHub org repos and rewrites the auto-generated section of REPOSITORY_INVENTORY.md
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\GitPlatformAdapter;
use MokoEnterprise\PlatformAdapterFactory;
/**
* Queries the git platform API for all repositories in the organisation and
* rewrites the auto-generated inventory table inside
* docs/reference/REPOSITORY_INVENTORY.md between the markers:
*
* <!-- INVENTORY_TABLE_START -->
* <!-- INVENTORY_TABLE_END -->
*
* Everything outside those markers is left untouched.
*/
class UpdateRepoInventory extends CliFramework
{
private ?GitPlatformAdapter $adapter = null;
/** Marker that begins the auto-generated block. */
private const MARKER_START = '<!-- INVENTORY_TABLE_START -->';
/** Marker that ends the auto-generated block. */
private const MARKER_END = '<!-- INVENTORY_TABLE_END -->';
/** Path to the Dolibarr module registry relative to repo root. */
private const REGISTRY_PATH = 'docs/development/crm/module-registry.md';
/** Path to the inventory file relative to repo root. */
private const INVENTORY_PATH = 'docs/reference/REPOSITORY_INVENTORY.md';
protected function configure(): void
{
$this->setDescription('Updates docs/reference/REPOSITORY_INVENTORY.md with current org repo list');
$this->addArgument('--org', 'Organisation to query', 'mokoconsulting-tech');
$this->addArgument('--path', 'Repository root path', '.');
}
protected function run(): int
{
$root = rtrim((string) $this->getArgument('--path'), '/\\');
$config = Config::load();
try {
$this->adapter = PlatformAdapterFactory::create($config);
} catch (\RuntimeException $e) {
$this->status(false, 'auth', $e->getMessage());
return 2;
}
$orgArg = (string) $this->getArgument('--org');
$org = $orgArg ?: $config->getString($this->adapter->getPlatformName() . '.organization', 'mokoconsulting-tech');
// ── 1. Fetch repositories ─────────────────────────────────────────────
$this->section("Fetching repositories for {$org} ({$this->adapter->getPlatformName()})");
$repos = $this->fetchAllRepos($org);
if ($repos === null) {
return 1;
}
$this->status(true, 'API', sprintf('Fetched %d repositories', count($repos)));
// ── 2. Load Dolibarr module registry ──────────────────────────────────
$this->section('Loading Dolibarr module registry');
$moduleMap = $this->parseModuleRegistry($root . '/' . self::REGISTRY_PATH);
$this->status(true, 'registry', sprintf('Loaded %d module ID entries', count($moduleMap)));
// ── 3. Build the Markdown tables ──────────────────────────────────────
$this->section('Building inventory tables');
$table = $this->buildTables($repos, $moduleMap, $org);
// ── 4. Rewrite the inventory file ────────────────────────────────────
$this->section('Updating ' . self::INVENTORY_PATH);
$inventoryPath = $root . '/' . self::INVENTORY_PATH;
if (!is_file($inventoryPath)) {
$this->status(false, self::INVENTORY_PATH, 'file not found');
return 2;
}
$original = (string) file_get_contents($inventoryPath);
$updated = $this->replaceSection($original, $table);
if ($original === $updated) {
$this->status(true, self::INVENTORY_PATH, 'no changes needed');
} elseif (!$this->isDryRun()) {
file_put_contents($inventoryPath, $updated);
$this->status(true, self::INVENTORY_PATH, 'updated');
} else {
$this->status(true, self::INVENTORY_PATH, '[dry-run] would update');
}
$this->printSummary(1, 0, $this->elapsed());
return 0;
}
// ── Platform API ──────────────────────────────────────────────────────────
/**
* Fetch all repositories for the org via the platform adapter.
*
* @return list<array<string,mixed>>|null Null on API error.
*/
private function fetchAllRepos(string $org): ?array
{
try {
// Use the adapter's paginated listing — returns full repo objects
$repos = $this->adapter->paginateAll("/orgs/{$org}/repos", ['type' => 'all']);
$this->progress(count($repos), count($repos), '', true);
return $repos;
} catch (\Exception $e) {
$this->status(false, 'API', $e->getMessage());
return null;
}
}
// ── Module registry ───────────────────────────────────────────────────────
/**
* Parse the Dolibarr module registry Markdown table.
*
* @return array<string,int> Map of lower-case repo name → module number.
*/
private function parseModuleRegistry(string $path): array
{
if (!is_file($path)) {
$this->warning("Module registry not found: {$path}");
return [];
}
$map = [];
$content = (string) file_get_contents($path);
// Match table rows: | ModuleName | 185051 | Status | … |
preg_match_all('/^\|\s*(\w+)\s*\|\s*(\d{6})\s*\|/m', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$id = (int) $match[2];
if ($id >= 100000) {
$map[strtolower($match[1])] = $id;
}
}
return $map;
}
// ── Table builder ─────────────────────────────────────────────────────────
/**
* Build the full Markdown replacement for the inventory tables.
*
* @param list<array<string,mixed>> $repos
* @param array<string,int> $moduleMap
*/
private function buildTables(array $repos, array $moduleMap, string $org): string
{
// Sort: active first, then archived; within each group alphabetically.
usort($repos, static function (array $a, array $b): int {
$aArch = (bool) ($a['archived'] ?? false);
$bArch = (bool) ($b['archived'] ?? false);
if ($aArch !== $bArch) {
return $aArch ? 1 : -1;
}
return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
});
/** @var array<string,list<array<string,mixed>>> $groups */
$groups = ['core' => [], 'product' => [], 'extension' => [], 'template' => [], 'internal' => [], 'archived' => []];
foreach ($repos as $repo) {
$name = (string) ($repo['name'] ?? '');
$topics = array_map('strtolower', (array) ($repo['topics'] ?? []));
$archived = (bool) ($repo['archived'] ?? false);
if ($archived) {
$groups['archived'][] = $repo;
continue;
}
$lower = strtolower($name);
if (in_array('mokostandards-core', $topics, true) || $name === 'MokoStandards' || $name === '.github-private') {
$groups['core'][] = $repo;
} elseif (
in_array('dolibarr-module', $topics, true)
|| str_starts_with($lower, 'mokodoli')
|| (str_starts_with($lower, 'mokocrm') && $lower !== 'mokocrmtheme')
) {
$groups['extension'][] = $repo;
} elseif (in_array('product', $topics, true) || in_array('platform', $topics, true)) {
$groups['product'][] = $repo;
} elseif (in_array('template', $topics, true) || str_contains($lower, 'template')) {
$groups['template'][] = $repo;
} else {
$groups['internal'][] = $repo;
}
}
$updated = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T');
$lines = [
"> ⚙️ **Auto-generated** by `update_repo_inventory.php` — last updated {$updated}.",
'> Do not edit this section manually; it is overwritten on every bulk sync.',
'',
];
$groupLabels = [
'core' => 'Core Repositories',
'product' => 'Product Repositories',
'extension' => 'Extension Repositories (Dolibarr / CRM)',
'template' => 'Template Repositories',
'internal' => 'Internal and Testing',
'archived' => 'Archived Repositories',
];
foreach ($groupLabels as $key => $label) {
if (empty($groups[$key])) {
continue;
}
$lines[] = "### {$label}";
$lines[] = '';
$isExt = ($key === 'extension');
if ($isExt) {
$lines[] = '| Repository | Status | Description | Module ID | Language | Visibility |';
$lines[] = '|------------|--------|-------------|-----------|----------|------------|';
} else {
$lines[] = '| Repository | Status | Description | Language | Visibility |';
$lines[] = '|------------|--------|-------------|----------|------------|';
}
foreach ($groups[$key] as $repo) {
$name = (string) ($repo['name'] ?? '');
$desc = str_replace('|', '\\|', (string) ($repo['description'] ?? ''));
$url = (string) ($repo['html_url'] ?? "https://github.com/{$org}/{$name}");
$lang = (string) ($repo['language'] ?? '—');
$private = (bool) ($repo['private'] ?? false);
$archived = (bool) ($repo['archived'] ?? false);
$status = $archived ? '🗄 Archived' : '✅ Active';
$vis = $private ? 'Private' : 'Public';
$modId = $moduleMap[strtolower($name)] ?? null;
$modCell = $modId !== null ? (string) $modId : '—';
if ($isExt) {
$lines[] = "| [{$name}]({$url}) | {$status} | {$desc} | {$modCell} | {$lang} | {$vis} |";
} else {
$lines[] = "| [{$name}]({$url}) | {$status} | {$desc} | {$lang} | {$vis} |";
}
}
$lines[] = '';
}
return implode("\n", $lines);
}
// ── File rewriter ─────────────────────────────────────────────────────────
/**
* Replace content between the start/end markers in the inventory file.
* If markers are absent, appends a new section at the end.
*/
private function replaceSection(string $original, string $newContent): string
{
$startPos = strpos($original, self::MARKER_START);
$endPos = strpos($original, self::MARKER_END);
if ($startPos === false || $endPos === false) {
$this->warning('Inventory markers not found; appending section to end of file.');
return $original
. "\n\n## Active Repositories\n\n"
. self::MARKER_START . "\n"
. $newContent . "\n"
. self::MARKER_END . "\n";
}
$before = substr($original, 0, $startPos + strlen(self::MARKER_START));
$after = substr($original, $endPos);
return $before . "\n" . $newContent . "\n" . $after;
}
}
$script = new UpdateRepoInventory('update_repo_inventory', 'Updates the repository inventory documentation after sync');
exit($script->execute());