chore: cascade main → dev (f8c28f0) [skip ci]
#166
@@ -0,0 +1,282 @@
|
||||
#!/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: 01.00.00
|
||||
* BRIEF: Sync select wiki pages from moko-platform to all template repos
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class WikiSync
|
||||
{
|
||||
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 $dryRun = false;
|
||||
private bool $allTemplates = false;
|
||||
|
||||
private int $synced = 0;
|
||||
private int $created = 0;
|
||||
private int $skipped = 0;
|
||||
private int $errors = 0;
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$this->parseArgs();
|
||||
|
||||
if ($this->token === '') {
|
||||
$this->log('ERROR: --token is required.');
|
||||
$this->printUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (empty($this->pages) && !$this->allTemplates) {
|
||||
$this->log('ERROR: --page or --all-standards is required.');
|
||||
$this->printUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Discover template repos if --all-templates
|
||||
if ($this->allTemplates || empty($this->targetRepos)) {
|
||||
$this->targetRepos = $this->discoverTemplateRepos();
|
||||
}
|
||||
|
||||
if (empty($this->targetRepos)) {
|
||||
$this->log('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("Syncing " . count($this->pages) . " page(s) to " . count($this->targetRepos) . " repo(s)");
|
||||
if ($this->dryRun) {
|
||||
$this->log("[DRY RUN] No changes will be made.\n");
|
||||
}
|
||||
|
||||
foreach ($this->pages as $pageName) {
|
||||
$this->log("\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(" {$repo}: IDENTICAL (skipped)");
|
||||
$this->skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->dryRun) {
|
||||
$action = $existing !== null ? 'WOULD UPDATE' : 'WOULD CREATE';
|
||||
$this->log(" {$repo}: {$action}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existing !== null) {
|
||||
$ok = $this->updateWikiPage($repo, $pageName, $sourceContent);
|
||||
$this->log(" {$repo}: " . ($ok ? 'UPDATED' : 'ERROR'));
|
||||
$ok ? $this->synced++ : $this->errors++;
|
||||
} else {
|
||||
$ok = $this->createWikiPage($repo, $pageName, $sourceContent);
|
||||
$this->log(" {$repo}: " . ($ok ? 'CREATED' : 'ERROR'));
|
||||
$ok ? $this->created++ : $this->errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log("\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("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("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;
|
||||
}
|
||||
|
||||
private function parseArgs(): void
|
||||
{
|
||||
global $argv;
|
||||
$args = $argv;
|
||||
for ($i = 1; $i < count($args); $i++) {
|
||||
switch ($args[$i]) {
|
||||
case '--token':
|
||||
$this->token = $args[++$i] ?? '';
|
||||
break;
|
||||
case '--org':
|
||||
$this->org = $args[++$i] ?? '';
|
||||
break;
|
||||
case '--source':
|
||||
$this->sourceRepo = $args[++$i] ?? '';
|
||||
break;
|
||||
case '--target':
|
||||
$this->targetRepos[] = $args[++$i] ?? '';
|
||||
break;
|
||||
case '--page':
|
||||
$this->pages[] = $args[++$i] ?? '';
|
||||
break;
|
||||
case '--all-standards':
|
||||
$this->pages = []; // will be populated from source wiki
|
||||
$this->allTemplates = true;
|
||||
break;
|
||||
case '--all-templates':
|
||||
$this->allTemplates = true;
|
||||
break;
|
||||
case '--dry-run':
|
||||
$this->dryRun = true;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
$this->printUsage();
|
||||
exit(0);
|
||||
default:
|
||||
$this->log("WARNING: Unknown argument: {$args[$i]}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function printUsage(): void
|
||||
{
|
||||
$this->log('Usage: wiki_sync.php --token <token> [options]');
|
||||
$this->log('');
|
||||
$this->log('Sync wiki pages from moko-platform to template repos.');
|
||||
$this->log('');
|
||||
$this->log('Options:');
|
||||
$this->log(' --token <token> Gitea API token (required)');
|
||||
$this->log(' --org <org> Organization (default: MokoConsulting)');
|
||||
$this->log(' --source <repo> Source repo (default: moko-platform)');
|
||||
$this->log(' --target <repo> Target repo (can repeat; default: all Template-* repos)');
|
||||
$this->log(' --page <name> Page to sync (can repeat)');
|
||||
$this->log(' --all-standards Sync all UPPERCASE standards pages');
|
||||
$this->log(' --all-templates Target all Template-* repos');
|
||||
$this->log(' --dry-run Show what would be done');
|
||||
$this->log(' --help, -h Show this help');
|
||||
$this->log('');
|
||||
$this->log('Examples:');
|
||||
$this->log(' php wiki_sync.php --token xxx --page MANIFEST_STANDARD --all-templates');
|
||||
$this->log(' php wiki_sync.php --token xxx --all-standards --all-templates --dry-run');
|
||||
$this->log(' php wiki_sync.php --token xxx --page WORKFLOW_STANDARDS --target Template-Joomla');
|
||||
}
|
||||
|
||||
private function log(string $msg): void
|
||||
{
|
||||
fwrite(STDERR, $msg . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
(new WikiSync())->run();
|
||||
@@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
MokoStandards Manifest Schema v1.0
|
||||
Defines the structure of .mokogitea/manifest.xml
|
||||
|
||||
Validate: xmllint - -schema definitions/manifest-schema.xsd .mokogitea/manifest.xml
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:moko="https://standards.mokoconsulting.tech/moko-platform/1.0"
|
||||
targetNamespace="https://standards.mokoconsulting.tech/moko-platform/1.0"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<!-- Root element -->
|
||||
<xs:element name="moko-platform">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="identity" type="moko:identityType"/>
|
||||
<xs:element name="governance" type="moko:governanceType"/>
|
||||
<xs:element name="build" type="moko:buildType"/>
|
||||
<xs:element name="deploy" type="moko:deployType" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="schema-version" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<!-- Identity block -->
|
||||
<xs:complexType name="identityType">
|
||||
<xs:sequence>
|
||||
<xs:element name="name" type="xs:string"/>
|
||||
<xs:element name="org" type="xs:string"/>
|
||||
<xs:element name="description" type="xs:string"/>
|
||||
<xs:element name="version" type="moko:versionType"/>
|
||||
<xs:element name="license" type="moko:licenseType"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Version format: XX.YY.ZZ -->
|
||||
<xs:simpleType name="versionType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="\d{2}\.\d{2}\.\d{2}"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- License with SPDX attribute -->
|
||||
<xs:complexType name="licenseType">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="spdx" type="xs:string" use="required"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Governance block -->
|
||||
<xs:complexType name="governanceType">
|
||||
<xs:sequence>
|
||||
<xs:element name="platform" type="moko:platformType"/>
|
||||
<xs:element name="standards-version" type="moko:versionType"/>
|
||||
<xs:element name="standards-source" type="xs:anyURI"/>
|
||||
<xs:element name="last-synced" type="xs:dateTime" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Allowed platform values -->
|
||||
<xs:simpleType name="platformType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="joomla"/>
|
||||
<xs:enumeration value="dolibarr"/>
|
||||
<xs:enumeration value="go"/>
|
||||
<xs:enumeration value="node"/>
|
||||
<xs:enumeration value="rust"/>
|
||||
<xs:enumeration value="python"/>
|
||||
<xs:enumeration value="generic"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Build block -->
|
||||
<xs:complexType name="buildType">
|
||||
<xs:sequence>
|
||||
<xs:element name="language" type="moko:languageType"/>
|
||||
<xs:element name="package-type" type="moko:packageType"/>
|
||||
<xs:element name="entry-point" type="xs:string"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Allowed languages -->
|
||||
<xs:simpleType name="languageType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="PHP"/>
|
||||
<xs:enumeration value="Go"/>
|
||||
<xs:enumeration value="JavaScript"/>
|
||||
<xs:enumeration value="TypeScript"/>
|
||||
<xs:enumeration value="Rust"/>
|
||||
<xs:enumeration value="Python"/>
|
||||
<xs:enumeration value="HCL"/>
|
||||
<xs:enumeration value="Shell"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Allowed package types -->
|
||||
<xs:simpleType name="packageType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="joomla-extension"/>
|
||||
<xs:enumeration value="dolibarr"/>
|
||||
<xs:enumeration value="application"/>
|
||||
<xs:enumeration value="library"/>
|
||||
<xs:enumeration value="mcp-server"/>
|
||||
<xs:enumeration value="generic"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Deploy block (optional) -->
|
||||
<xs:complexType name="deployType">
|
||||
<xs:sequence>
|
||||
<xs:element name="source-dir" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="remote-subdir" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="excludes" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="dev-host" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="demo-host" type="xs:string" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
||||
Reference in New Issue
Block a user