Public Access
docs: update for consolidated Joomla template repo
- Update WORKFLOW_STANDARDS.md to reference MokoStandards-Template-Joomla - Remove 6 obsolete sync definitions for deleted individual template repos - Update sync commands to use unified template Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,12 +39,13 @@ class RepositorySynchronizer
|
||||
private const VERSION_BRANCH = 'version/' . self::STANDARDS_MAJOR;
|
||||
private const SYNC_BRANCH = 'chore/sync-mokostandards-v' . self::STANDARDS_MINOR;
|
||||
|
||||
private ApiClient $apiClient;
|
||||
private GitPlatformAdapter $adapter;
|
||||
private AuditLogger $logger;
|
||||
private MetricsCollector $metrics;
|
||||
private CheckpointManager $checkpoints;
|
||||
private DefinitionParser $definitionParser;
|
||||
private ApiClient $apiClient;
|
||||
private GitPlatformAdapter $adapter;
|
||||
private AuditLogger $logger;
|
||||
private MetricsCollector $metrics;
|
||||
private CheckpointManager $checkpoints;
|
||||
private DefinitionParser $definitionParser;
|
||||
private MokoStandardsParser $manifestParser;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -70,6 +71,7 @@ class RepositorySynchronizer
|
||||
$this->metrics = $metrics;
|
||||
$this->checkpoints = $checkpoints ?? new CheckpointManager('.checkpoints');
|
||||
$this->definitionParser = $definitionParser ?? new DefinitionParser();
|
||||
$this->manifestParser = new MokoStandardsParser();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,7 +402,65 @@ HCL;
|
||||
/** Repos that are the full Dolibarr platform, not individual modules. */
|
||||
private const CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
|
||||
|
||||
/**
|
||||
* Detect platform from the .mokostandards manifest (authoritative), falling
|
||||
* back to name/topic/description heuristics when the manifest is missing or
|
||||
* unparseable.
|
||||
*/
|
||||
private function detectPlatform(array $repoInfo): string
|
||||
{
|
||||
$org = $repoInfo['full_name'] ? explode('/', $repoInfo['full_name'])[0] : '';
|
||||
$name = $repoInfo['name'] ?? '';
|
||||
|
||||
// ── 1. Try reading the XML .mokostandards manifest ────────────��─
|
||||
$manifestPlatform = $this->readManifestPlatform($org, $name);
|
||||
if ($manifestPlatform !== null) {
|
||||
$this->logger->logInfo("Platform for {$name} from .mokostandards manifest: {$manifestPlatform}");
|
||||
return $manifestPlatform;
|
||||
}
|
||||
|
||||
// ── 2. Fallback: heuristic detection ────────────────────────────
|
||||
return $this->detectPlatformByHeuristics($repoInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the platform slug from the remote .mokostandards manifest.
|
||||
* Checks .gitea/.mokostandards, .github/.mokostandards, and root .mokostandards.
|
||||
*
|
||||
* @return string|null Platform slug or null if not found/parseable
|
||||
*/
|
||||
private function readManifestPlatform(string $org, string $repo): ?string
|
||||
{
|
||||
$metaDir = $this->adapter->getMetadataDir();
|
||||
$paths = [
|
||||
"{$metaDir}/.mokostandards",
|
||||
'.mokostandards',
|
||||
];
|
||||
if ($metaDir === '.gitea') {
|
||||
$paths[] = '.github/.mokostandards';
|
||||
}
|
||||
|
||||
foreach ($paths as $path) {
|
||||
try {
|
||||
$file = $this->adapter->getFileContents($org, $repo, $path);
|
||||
$content = base64_decode($file['content'] ?? '');
|
||||
$platform = $this->manifestParser->extractPlatform($content);
|
||||
if ($platform !== null && in_array($platform, MokoStandardsParser::VALID_PLATFORMS, true)) {
|
||||
return $platform;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic platform detection from repo name, topics, and description.
|
||||
* Used as fallback when .mokostandards manifest is missing or unparseable.
|
||||
*/
|
||||
private function detectPlatformByHeuristics(array $repoInfo): string
|
||||
{
|
||||
$name = $repoInfo['name'] ?? '';
|
||||
$nameLower = strtolower($name);
|
||||
@@ -448,7 +508,7 @@ HCL;
|
||||
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) {
|
||||
return 'crm-module';
|
||||
}
|
||||
|
||||
|
||||
// Default
|
||||
return 'default-repository';
|
||||
}
|
||||
@@ -503,8 +563,8 @@ HCL;
|
||||
// Ensure composer.json requires mokoconsulting-tech/enterprise (default branch only)
|
||||
$this->ensureComposerEnterprise($org, $repo, $defaultBranch, $summary);
|
||||
|
||||
// Migrate .mokostandards (default branch only)
|
||||
$this->migrateMokoStandards($org, $repo, $defaultBranch, $summary);
|
||||
// Migrate .mokostandards to XML manifest (default branch only)
|
||||
$this->migrateMokoStandards($org, $repo, $defaultBranch, $platform, $repoInfo, $summary);
|
||||
|
||||
if (count($summary['copied']) === 0) {
|
||||
$this->logger->logWarning("No files were created/updated for {$repo}");
|
||||
@@ -706,80 +766,229 @@ HCL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate .mokostandards to the platform metadata dir (.gitea/ or .github/).
|
||||
* Handles migration from root and from .github/ → .gitea/ on Gitea.
|
||||
* Migrate .mokostandards to the platform metadata dir (.gitea/ or .github/)
|
||||
* and convert legacy YAML-like format to the new XML manifest.
|
||||
*
|
||||
* Handles:
|
||||
* 1. Location migration: root or .github/ → .gitea/.mokostandards
|
||||
* 2. Format migration: legacy "platform: xxx" → XML manifest
|
||||
* 3. Update existing XML: refresh <governance><last-synced> timestamp
|
||||
*/
|
||||
private function migrateMokoStandards(string $org, string $repo, string $branchName, array &$summary): void
|
||||
{
|
||||
$metaDir = $this->adapter->getMetadataDir();
|
||||
private function migrateMokoStandards(
|
||||
string $org,
|
||||
string $repo,
|
||||
string $branchName,
|
||||
string $platform,
|
||||
array $repoInfo,
|
||||
array &$summary
|
||||
): void {
|
||||
$metaDir = $this->adapter->getMetadataDir();
|
||||
$targetPath = "{$metaDir}/.mokostandards";
|
||||
|
||||
// Sources to check, in priority order
|
||||
$sources = ['.mokostandards'];
|
||||
// On Gitea, also migrate from .github/.mokostandards → .gitea/.mokostandards
|
||||
// ── Collect existing files from all legacy locations ─────────
|
||||
$legacySources = ['.mokostandards'];
|
||||
if ($metaDir === '.gitea') {
|
||||
$sources[] = '.github/.mokostandards';
|
||||
$legacySources[] = '.github/.mokostandards';
|
||||
}
|
||||
|
||||
$rootFile = null;
|
||||
$sourcePath = null;
|
||||
foreach ($sources as $path) {
|
||||
$legacyFiles = []; // path => ['content' => raw, 'sha' => sha]
|
||||
foreach ($legacySources as $path) {
|
||||
try {
|
||||
$rootFile = $this->adapter->getFileContents($org, $repo, $path, $branchName);
|
||||
$sourcePath = $path;
|
||||
break;
|
||||
$file = $this->adapter->getFileContents($org, $repo, $path, $branchName);
|
||||
$legacyFiles[$path] = [
|
||||
'content' => base64_decode($file['content'] ?? ''),
|
||||
'sha' => $file['sha'] ?? '',
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
|
||||
if ($rootFile === null) {
|
||||
return; // Nothing to migrate
|
||||
}
|
||||
|
||||
// Check if already exists in metadata dir
|
||||
$existsInMetaDir = false;
|
||||
// Check if target already exists in metadata dir
|
||||
$existingTarget = null;
|
||||
try {
|
||||
$this->adapter->getFileContents($org, $repo, $targetPath, $branchName);
|
||||
$existsInMetaDir = true;
|
||||
$file = $this->adapter->getFileContents($org, $repo, $targetPath, $branchName);
|
||||
$existingTarget = [
|
||||
'content' => base64_decode($file['content'] ?? ''),
|
||||
'sha' => $file['sha'] ?? '',
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
|
||||
$content = base64_decode($rootFile['content'] ?? '');
|
||||
$rootSha = $rootFile['sha'] ?? '';
|
||||
// ── Determine the best existing content to work from ────────
|
||||
$currentContent = $existingTarget['content'] ?? null;
|
||||
if ($currentContent === null) {
|
||||
// Pick from legacy sources (first found)
|
||||
foreach ($legacyFiles as $data) {
|
||||
$currentContent = $data['content'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Generate the new XML manifest ───────────────────────────
|
||||
$xmlContent = $this->generateMokoStandardsXml(
|
||||
$org,
|
||||
$repo,
|
||||
$platform,
|
||||
$repoInfo,
|
||||
$currentContent
|
||||
);
|
||||
|
||||
// ── Write to target path ────────────────────────────────────
|
||||
$targetSha = $existingTarget['sha'] ?? null;
|
||||
$isNew = $existingTarget === null;
|
||||
$needsUpdate = $isNew || $existingTarget['content'] !== $xmlContent;
|
||||
|
||||
if ($needsUpdate) {
|
||||
$action = $isNew ? 'create' : 'update';
|
||||
$commitMsg = $isNew
|
||||
? "chore: add XML .mokostandards manifest to {$metaDir}/"
|
||||
: "chore: update .mokostandards manifest (XML format)";
|
||||
|
||||
if (!$existsInMetaDir) {
|
||||
// Copy to metadata dir
|
||||
try {
|
||||
$this->adapter->createOrUpdateFile(
|
||||
$org, $repo, $targetPath, $content,
|
||||
"chore: migrate .mokostandards to {$metaDir}/",
|
||||
null, $branchName
|
||||
$org, $repo, $targetPath, $xmlContent,
|
||||
$commitMsg, $targetSha, $branchName
|
||||
);
|
||||
$this->logger->logInfo("Migrated .mokostandards → {$targetPath}");
|
||||
$summary['copied'][] = ['file' => $targetPath, 'action' => 'migrated from root'];
|
||||
$this->logger->logInfo(ucfirst($action) . "d XML .mokostandards → {$targetPath}");
|
||||
$summary['copied'][] = ['file' => $targetPath, 'action' => "{$action}d (XML manifest)"];
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
$this->logger->logWarning("Could not {$action} .mokostandards: " . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old source file
|
||||
if (!empty($rootSha) && $sourcePath !== $targetPath) {
|
||||
// ── Delete legacy source files ──────────────────────────────
|
||||
foreach ($legacyFiles as $path => $data) {
|
||||
if ($path === $targetPath || empty($data['sha'])) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->adapter->deleteFile(
|
||||
$org, $repo, $sourcePath, $rootSha,
|
||||
"chore: remove {$sourcePath} (moved to {$targetPath})",
|
||||
$org, $repo, $path, $data['sha'],
|
||||
"chore: remove legacy {$path} (replaced by {$targetPath})",
|
||||
$branchName
|
||||
);
|
||||
$this->logger->logInfo("Deleted {$sourcePath}");
|
||||
$this->logger->logInfo("Deleted legacy {$path}");
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an XML .mokostandards manifest for a repository.
|
||||
*
|
||||
* If existing content is valid XML, preserves user-edited sections
|
||||
* (build, deploy, scripts, overrides) and only refreshes governance metadata.
|
||||
*
|
||||
* @param string $org Organization name
|
||||
* @param string $repo Repository name
|
||||
* @param string $platform Detected platform slug
|
||||
* @param array $repoInfo Gitea API repo object
|
||||
* @param string|null $existingContent Current .mokostandards content (XML or legacy)
|
||||
* @return string Well-formed XML content
|
||||
*/
|
||||
private function generateMokoStandardsXml(
|
||||
string $org,
|
||||
string $repo,
|
||||
string $platform,
|
||||
array $repoInfo,
|
||||
?string $existingContent
|
||||
): string {
|
||||
$params = [
|
||||
'name' => $repoInfo['name'] ?? $repo,
|
||||
'org' => $org,
|
||||
'platform' => $platform,
|
||||
'standards_version' => self::STANDARDS_VERSION,
|
||||
'description' => $repoInfo['description'] ?? '',
|
||||
'license' => 'GPL-3.0-or-later',
|
||||
'topics' => $repoInfo['topics'] ?? [],
|
||||
'language' => $repoInfo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
||||
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
||||
'last_synced' => date('c'),
|
||||
];
|
||||
|
||||
// If existing content is already valid XML, try to preserve user sections
|
||||
if ($existingContent !== null && str_contains($existingContent, '<mokostandards')) {
|
||||
try {
|
||||
$existing = $this->manifestParser->parse($existingContent);
|
||||
|
||||
// Preserve user-edited build, deploy, scripts, overrides by re-emitting
|
||||
// the existing XML with only governance fields refreshed.
|
||||
// For now, we use the simple generate() which creates identity + governance + build.
|
||||
// User-managed sections (deploy, scripts, overrides) are preserved by doing
|
||||
// a targeted replacement of governance fields in the existing XML.
|
||||
return $this->refreshGovernanceInXml(
|
||||
$existingContent,
|
||||
$platform,
|
||||
self::STANDARDS_VERSION,
|
||||
date('c')
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
// Existing XML is broken — regenerate from scratch
|
||||
$this->logger->logInfo("Existing .mokostandards XML invalid, regenerating: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->manifestParser->generate($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh only the <governance> fields in an existing XML .mokostandards,
|
||||
* preserving all other sections (build, deploy, scripts, overrides).
|
||||
*/
|
||||
private function refreshGovernanceInXml(
|
||||
string $xml,
|
||||
string $platform,
|
||||
string $standardsVersion,
|
||||
string $lastSynced
|
||||
): string {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = true;
|
||||
$dom->formatOutput = true;
|
||||
|
||||
if (!$dom->loadXML($xml)) {
|
||||
// If parsing fails, return as-is
|
||||
return $xml;
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('m', MokoStandardsParser::NAMESPACE_URI);
|
||||
|
||||
// Update <platform>
|
||||
$nodes = $xpath->query('//m:governance/m:platform');
|
||||
if ($nodes->length > 0) {
|
||||
$nodes->item(0)->textContent = $platform;
|
||||
}
|
||||
|
||||
// Update <standards-version>
|
||||
$nodes = $xpath->query('//m:governance/m:standards-version');
|
||||
if ($nodes->length > 0) {
|
||||
$nodes->item(0)->textContent = $standardsVersion;
|
||||
}
|
||||
|
||||
// Update or create <last-synced>
|
||||
$nodes = $xpath->query('//m:governance/m:last-synced');
|
||||
if ($nodes->length > 0) {
|
||||
$nodes->item(0)->textContent = $lastSynced;
|
||||
} else {
|
||||
$govNodes = $xpath->query('//m:governance');
|
||||
if ($govNodes->length > 0) {
|
||||
$lastSyncedEl = $dom->createElementNS(
|
||||
MokoStandardsParser::NAMESPACE_URI,
|
||||
'last-synced'
|
||||
);
|
||||
$lastSyncedEl->textContent = $lastSynced;
|
||||
$govNodes->item(0)->appendChild($lastSyncedEl);
|
||||
}
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
private function ensureComposerEnterprise(string $org, string $repo, string $branchName, array &$summary): void
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user