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>
311 lines
11 KiB
PHP
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());
|