Files
moko-platform/cli/wiki_sync.php
T
Jonathan Miller b3d9ee8255
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
refactor(cli): migrate 64 legacy scripts to CliFramework (#235)
Wrap all CLI tools in cli/, automation/, maintenance/, deploy/, and
release/ in classes extending CliFramework. Replaces manual $argv
parsing with configure()/addArgument(), moves logic into run(): int,
and converts fwrite(STDERR,...) to $this->log(). Two CLIApp subclasses
(generate_dolibarr_version_txt, generate_joomla_update_xml) converted
to extend CliFramework directly.

Every script now gets free --help, --verbose, --quiet, --dry-run,
--json, --no-color, banners, coloured logging, and progress bars.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:39:10 -05:00

243 lines
8.8 KiB
PHP

#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/wiki_sync.php
* VERSION: 09.21.07
* BRIEF: Sync select wiki pages from moko-platform to all template repos
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
class WikiSyncCli extends CliFramework
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private string $org = 'MokoConsulting';
private string $sourceRepo = 'moko-platform';
private array $targetRepos = [];
private array $pages = [];
private bool $allTemplates = false;
private bool $allStandards = false;
private int $synced = 0;
private int $created = 0;
private int $skipped = 0;
private int $errors = 0;
protected function configure(): void
{
$this->setDescription('Sync wiki pages from moko-platform to template repos');
$this->addArgument('--token', 'Gitea API token (required)', '');
$this->addArgument('--org', 'Organization (default: MokoConsulting)', 'MokoConsulting');
$this->addArgument('--source', 'Source repo (default: moko-platform)', 'moko-platform');
$this->addArgument('--target', 'Target repo (can repeat)', '');
$this->addArgument('--page', 'Page to sync (can repeat)', '');
$this->addArgument('--all-standards', 'Sync all UPPERCASE standards pages', false);
$this->addArgument('--all-templates', 'Target all Template-* repos', false);
}
protected function run(): int
{
$this->token = $this->getArgument('--token');
$this->org = $this->getArgument('--org');
$this->sourceRepo = $this->getArgument('--source');
$this->allStandards = (bool) $this->getArgument('--all-standards');
$this->allTemplates = (bool) $this->getArgument('--all-templates');
// Handle repeatable args from raw argv
global $argv;
foreach ($argv as $i => $arg) {
if ($arg === '--target' && isset($argv[$i + 1])) {
$this->targetRepos[] = $argv[$i + 1];
}
if ($arg === '--page' && isset($argv[$i + 1])) {
$this->pages[] = $argv[$i + 1];
}
}
if ($this->token === '') {
$this->log('ERROR', '--token is required.');
return 1;
}
if (empty($this->pages) && !$this->allStandards) {
$this->log('ERROR', '--page or --all-standards is required.');
return 1;
}
// Discover template repos if --all-templates
if ($this->allTemplates || empty($this->targetRepos)) {
$this->targetRepos = $this->discoverTemplateRepos();
}
if (empty($this->targetRepos)) {
$this->log('INFO', 'No target repos found.');
return 0;
}
// If --all-standards, get all pages that start with uppercase
if (empty($this->pages)) {
$this->pages = $this->getStandardsPages();
}
$this->log('INFO', "Syncing " . count($this->pages) . " page(s) to " . count($this->targetRepos) . " repo(s)");
if ($this->dryRun) {
$this->log('INFO', "[DRY RUN] No changes will be made.\n");
}
foreach ($this->pages as $pageName) {
$this->log('INFO', "\n--- Page: {$pageName} ---");
$sourceContent = $this->getWikiPage($this->sourceRepo, $pageName);
if ($sourceContent === null) {
$this->log('WARNING', "page not found in {$this->sourceRepo}");
$this->errors++;
continue;
}
foreach ($this->targetRepos as $repo) {
$existing = $this->getWikiPage($repo, $pageName);
if ($existing !== null && $existing === $sourceContent) {
$this->log('INFO', " {$repo}: IDENTICAL (skipped)");
$this->skipped++;
continue;
}
if ($this->dryRun) {
$action = $existing !== null ? 'WOULD UPDATE' : 'WOULD CREATE';
$this->log('INFO', " {$repo}: {$action}");
continue;
}
if ($existing !== null) {
$ok = $this->updateWikiPage($repo, $pageName, $sourceContent);
$this->log('INFO', " {$repo}: " . ($ok ? 'UPDATED' : 'ERROR'));
$ok ? $this->synced++ : $this->errors++;
} else {
$ok = $this->createWikiPage($repo, $pageName, $sourceContent);
$this->log('INFO', " {$repo}: " . ($ok ? 'CREATED' : 'ERROR'));
$ok ? $this->created++ : $this->errors++;
}
}
}
$this->log('INFO', "\nDone: {$this->synced} updated, {$this->created} created, {$this->skipped} skipped, {$this->errors} error(s)");
return $this->errors > 0 ? 1 : 0;
}
private function discoverTemplateRepos(): array
{
$repos = $this->apiGet("/orgs/{$this->org}/repos?limit=100");
$templates = [];
foreach ($repos as $repo) {
if (str_starts_with($repo['name'], 'Template-') && !($repo['archived'] ?? false)) {
$templates[] = $repo['name'];
}
}
sort($templates);
$this->log('INFO', "Found template repos: " . implode(', ', $templates));
return $templates;
}
private function getStandardsPages(): array
{
$pages = $this->apiGet("/repos/{$this->org}/{$this->sourceRepo}/wiki/pages");
$standards = [];
foreach ($pages as $page) {
$title = $page['title'] ?? '';
// Sync pages that are all-caps with underscores (standards pages)
if (preg_match('/^[A-Z][A-Z0-9_-]+$/', $title)) {
$standards[] = $title;
}
}
sort($standards);
$this->log('INFO', "Found " . count($standards) . " standards pages: " . implode(', ', $standards));
return $standards;
}
private function getWikiPage(string $repo, string $pageName): ?string
{
$data = $this->apiGet("/repos/{$this->org}/{$repo}/wiki/page/{$pageName}");
if ($data === null || !isset($data['content_base64'])) {
return null;
}
return base64_decode($data['content_base64']);
}
private function createWikiPage(string $repo, string $pageName, string $content): bool
{
$payload = json_encode([
'title' => $pageName,
'content_base64' => base64_encode($content),
]);
return $this->apiPost("/repos/{$this->org}/{$repo}/wiki/new", $payload) !== null;
}
private function updateWikiPage(string $repo, string $pageName, string $content): bool
{
$payload = json_encode([
'title' => $pageName,
'content_base64' => base64_encode($content),
]);
return $this->apiPatch("/repos/{$this->org}/{$repo}/wiki/page/{$pageName}", $payload) !== null;
}
private function apiGet(string $endpoint): ?array
{
$url = "{$this->giteaUrl}/api/v1{$endpoint}";
$opts = [
'http' => [
'method' => 'GET',
'header' => "Authorization: token {$this->token}\r\nAccept: application/json\r\n",
'ignore_errors' => true,
],
];
$ctx = stream_context_create($opts);
$result = @file_get_contents($url, false, $ctx);
if ($result === false) return null;
$data = json_decode($result, true);
return is_array($data) ? $data : null;
}
private function apiPost(string $endpoint, string $payload): ?array
{
return $this->apiWrite('POST', $endpoint, $payload);
}
private function apiPatch(string $endpoint, string $payload): ?array
{
return $this->apiWrite('PATCH', $endpoint, $payload);
}
private function apiWrite(string $method, string $endpoint, string $payload): ?array
{
$url = "{$this->giteaUrl}/api/v1{$endpoint}";
$opts = [
'http' => [
'method' => $method,
'header' => "Authorization: token {$this->token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
'content' => $payload,
'ignore_errors' => true,
],
];
$ctx = stream_context_create($opts);
$result = @file_get_contents($url, false, $ctx);
if ($result === false) return null;
$data = json_decode($result, true);
return is_array($data) ? $data : null;
}
}
$app = new WikiSyncCli();
exit($app->execute());