refactor: MokoGitea -> MokoGit migration + workflow-sync repairs (#350 #351 #352 #354 #11 #4) #356

Merged
jmiller merged 15 commits from fix/mokogit-migration into dev 2026-07-16 22:08:39 +00:00
341 changed files with 2322 additions and 6511 deletions
+37 -896
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -70,7 +70,7 @@ PHPStan runs with `--memory-limit=512M`. CI enforces PHPCS errors; PHPStan is `c
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogit/` (not `.gitea/` or `.github/`)
- **Workflow directory**: `.mokogit/` (not `.mokogit/` or `.github/`)
- **Wiki**: documentation lives in the MokoGIT wiki, not `docs/` files
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
- **Standards**: [MokoCli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)
+1 -1
View File
@@ -4,7 +4,7 @@
# DEFGROUP: Git.Workflow
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/branch-protection.yml
# PATH: /.mokogit/workflows/branch-protection.yml
# BRIEF: Apply standardised branch protection rules to all governed repositories
#
# +========================================================================+
+1 -1
View File
@@ -4,7 +4,7 @@
# DEFGROUP: Git.Workflow
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/bulk-repo-sync.yml
# PATH: /.mokogit/workflows/bulk-repo-sync.yml
# BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos
name: Bulk Repository Sync
+1 -1
View File
@@ -5,7 +5,7 @@
# DEFGROUP: mokocli.CI
# INGROUP: mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/pr-branch-check.yml
# PATH: /.mokogit/workflows/pr-branch-check.yml
# BRIEF: PR branch merge policy enforcement
#
# Enforces branch merge policy:
+2 -2
View File
@@ -6,7 +6,7 @@
# DEFGROUP: Git.Workflow
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/renovate.yml
# PATH: /.mokogit/workflows/renovate.yml
# BRIEF: Run Renovate Bot across all governed repos for dependency updates
#
# +========================================================================+
@@ -109,7 +109,7 @@ jobs:
env:
RENOVATE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
RENOVATE_PLATFORM: git
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
RENOVATE_ENDPOINT: ${{ env.MOKOGIT_URL }}/api/v1
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
RENOVATE_REPOSITORIES: ${{ steps.repos.outputs.repo_list }}
RENOVATE_DRY_RUN: ${{ inputs.dry_run == 'true' && 'full' || 'null' }}
+2 -2
View File
@@ -6,7 +6,7 @@
# DEFGROUP: Git.Workflow
# INGROUP: mokocli.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/sync-wikis.yml
# PATH: /.mokogit/workflows/sync-wikis.yml
# BRIEF: Daily sync of all Git wikis to consolidated GitHub wiki repo
name: Sync Wikis to GitHub
@@ -32,7 +32,7 @@ jobs:
- name: Sync all wikis
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::error::GH_TOKEN secret not set"
+1 -1
View File
@@ -22,7 +22,7 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
+3 -3
View File
@@ -52,9 +52,9 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
permissions:
contents: write
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
REPO: ${{ github.repository }}
API_BASE: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
API_BASE: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
+1 -1
View File
@@ -27,7 +27,7 @@ permissions:
pull-requests: write
env:
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
# ntfy destination is configured via repo or org variables (org vars are inherited).
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'git-releases' }}
+2 -2
View File
@@ -51,14 +51,14 @@ jobs:
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
MOKOGIT_URL="${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
- name: Report CI failure
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
/tmp/mokocli/cli/ci_issue_reporter.sh \
+1 -1
View File
@@ -21,7 +21,7 @@ permissions:
contents: write
env:
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
+1 -1
View File
@@ -454,7 +454,7 @@ jobs:
needs.templates.result == 'failure')
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
REPORTER="./automation/ci-issue-reporter.sh"
@@ -13,7 +13,7 @@ on:
workflow_dispatch:
env:
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
jobs:
publish:
@@ -0,0 +1,58 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# CUSTOM WORKFLOW — lives in custom/ and is not managed by workflow sync.
#
# Keeps /opt/mokocli on the runner up to date with the latest main branch.
# Runs on schedule and when mokocli itself is released.
name: "Custom: Self-Update Runner"
on:
schedule:
- cron: '0 */6 * * *'
push:
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
update:
name: Update /opt/mokocli on runner
runs-on: release
steps:
- name: Pull latest mokocli
run: |
TARGET="/opt/mokocli"
if [ ! -d "${TARGET}/.git" ]; then
echo "Initial clone to ${TARGET}"
sudo rm -rf "${TARGET}"
sudo git clone --depth 1 --branch main \
"https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
"${TARGET}"
sudo chown -R "$(whoami)" "${TARGET}"
else
echo "Updating existing ${TARGET}"
cd "${TARGET}"
git fetch --depth 1 origin main
git reset --hard origin/main
fi
- name: Install dependencies
run: |
cd /opt/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Verify installation
run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo "mokocli updated successfully"
php /opt/mokocli/cli/version_bump.php --help 2>/dev/null | head -1 || echo "CLI available"
else
echo "::error::mokocli update failed — missing required files"
exit 1
fi
@@ -22,7 +22,7 @@ permissions:
contents: write
env:
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
jobs:
sync:
+1 -1
View File
@@ -19,7 +19,7 @@ permissions:
issues: write
env:
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
jobs:
create-branch:
+2 -2
View File
@@ -146,7 +146,7 @@ jobs:
- name: Remind to update the wiki
env:
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
@@ -581,7 +581,7 @@ jobs:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
+3 -3
View File
@@ -40,9 +40,9 @@ permissions:
contents: write
env:
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
jobs:
build:
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
env:
BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
set -euo pipefail
+2 -3
View File
@@ -34,8 +34,7 @@ feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- MokoGit release created with ZIP/tar.gz packages
- `dev` branch recreated from `main`
### Branch summary
@@ -119,7 +118,7 @@ This ensures Joomla sites on ANY stability channel see the update (Joomla only s
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- `.mokogit/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
+7 -7
View File
@@ -16,7 +16,7 @@ BRIEF: Project overview and documentation
PHP implementation of mokocli — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoCli-API)
> **Primary platform**: [MokoGit — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoCli-API)
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoCli-API) *(read-only mirror)*
## What Lives Here
@@ -25,7 +25,7 @@ PHP implementation of mokocli — enterprise standards, automation framework, wo
|-----------|---------|
| `lib/Enterprise/` | 38 PHP enterprise library classes (platform adapters, sync, validation, plugins) |
| `cli/` | CLI scripts (archive, create, release, sync rulesets, version management) |
| `automation/` | Bulk sync, push files, repo cleanup, Gitea migration |
| `automation/` | Bulk sync, push files, repo cleanup, MokoGit migration |
| `validate/` | 18 validation scripts (health, structure, secrets, syntax, drift) |
| `templates/` | **Workflow templates** and config templates synced to governed repos |
| `definitions/` | Repository structure definitions (`.tf` format) |
@@ -42,7 +42,7 @@ composer require mokoconsulting-tech/enterprise
### Composer Registry
This package is served from Gitea package registry. Add this to your `composer.json`:
This package is served from MokoGit package registry. Add this to your `composer.json`:
```json
{
@@ -72,11 +72,11 @@ vendor/bin/moko inventory -- --path .
| Variable | Purpose |
|----------|---------|
| `GIT_PLATFORM` | `gitea` (default) or `github` |
| `GA_TOKEN` | Gitea API / Gitea Actions token |
| `GIT_PLATFORM` | `mokogit` (default) or `github` |
| `GA_TOKEN` | MokoGit API / MokoGit Actions token |
| `GH_TOKEN` | GitHub API token (for mirror sync) |
| `GITEA_URL` | Gitea instance URL (default: `https://git.mokoconsulting.tech`) |
| `GITEA_ORG` | Gitea organization (default: `MokoConsulting`) |
| `MOKOGIT_URL` | MokoGit instance URL (default: `https://git.mokoconsulting.tech`) |
| `MOKOGIT_ORG` | MokoGit organization (default: `MokoConsulting`) |
## License
+3 -242
View File
@@ -45,7 +45,7 @@ use MokoCli\{
* --sync: Push mokocli files to existing template repositories
* --list: List all repositories tagged as joomla-template
*
* Works with both GitHub and Gitea via the PlatformAdapterFactory.
* Works with both GitHub and MokoGit via the PlatformAdapterFactory.
*/
class BulkJoomlaTemplate extends CliFramework
{
@@ -66,7 +66,6 @@ class BulkJoomlaTemplate extends CliFramework
$this->addArgument('--client', 'Joomla client: site or admin', 'site');
$this->addArgument('--repos', 'Target repos (comma-separated)', '');
$this->addArgument('--all', 'Sync all tagged repos', false);
$this->addArgument('--sync-updates', 'Sync updates.xml', false);
$this->addArgument('--private', 'Create as private repo', false);
$this->addArgument('--yes', 'Auto-confirm', false);
}
@@ -100,11 +99,7 @@ class BulkJoomlaTemplate extends CliFramework
return $this->syncTemplates($org);
}
if ($this->getArgument('--sync-updates', false)) {
return $this->syncUpdatesBetweenPlatforms($org);
}
$this->log("❌ Specify --scaffold, --sync, --sync-updates, or --list", 'ERROR');
$this->log("❌ Specify --scaffold, --sync, or --list", 'ERROR');
return 1;
}
@@ -376,7 +371,6 @@ class BulkJoomlaTemplate extends CliFramework
{
$files = [
'templateDetails.xml',
'updates.xml',
'src/index.php',
'src/error.php',
'src/offline.php',
@@ -461,10 +455,7 @@ class BulkJoomlaTemplate extends CliFramework
<updateservers>
<server type="extension" priority="1" name="{$name} Update Server">
https://git.mokoconsulting.tech/{$org}/{$name}/raw/branch/main/updates.xml
</server>
<server type="extension" priority="2" name="{$name} Update Server">
https://raw.githubusercontent.com/{$org}/{$name}/main/updates.xml
https://git.mokoconsulting.tech/{$org}/{$name}/updates.xml
</server>
</updateservers>
@@ -486,30 +477,6 @@ class BulkJoomlaTemplate extends CliFramework
XML;
$files['templateDetails.xml'] = preg_replace('/^\t\t/m', '', $files['templateDetails.xml']);
// updates.xml — dual-platform download URLs (Gitea primary, GitHub secondary)
$files['updates.xml'] = <<<XML
<updates>
<update>
<name>{$name}</name>
<description>{$name} — Moko Consulting Joomla template</description>
<element>tpl_{$shortName}</element>
<type>template</type>
<version>1.0.0</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/{$org}/{$name}/releases/download/v1.0.0/{$shortName}.zip
</downloadurl>
<downloadurl type="full" format="zip">
https://github.com/{$org}/{$name}/releases/download/v1.0.0/{$shortName}.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="[56].*"/>
<php_minimum>8.1</php_minimum>
</update>
</updates>
XML;
$files['updates.xml'] = preg_replace('/^\t\t/m', '', $files['updates.xml']);
// src/index.php
$files['src/index.php'] = <<<'PHP'
<?php
@@ -722,212 +689,6 @@ class BulkJoomlaTemplate extends CliFramework
return $files;
}
// ── Sync updates.xml between platforms ───────────────────────────────
/**
* Sync updates.xml (or updates.xml) between Gitea and GitHub for Joomla repos.
*
* Reads the file from both platforms, compares by latest <version> tag,
* and pushes the newer one to the stale platform.
*
* Designed to be called from a CI workflow via:
* php automation/bulk_joomla_template.php --sync-updates --repos=MokoCassiopeia
*/
private function syncUpdatesBetweenPlatforms(string $org): int
{
$repos = [];
if ($this->getArgument('--all', false)) {
$repos = $this->findTemplateRepos($org);
// Also include waas-component repos
$allRepos = $this->adapter->listOrgRepos($org, true);
foreach ($allRepos as $repo) {
try {
$topics = $this->adapter->getRepoTopics($org, $repo['name']);
if (in_array('joomla', $topics, true) || in_array('joomla-extension', $topics, true)) {
$repos[] = $repo;
}
} catch (\Exception $e) {
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
// Deduplicate
$seen = [];
$repos = array_filter($repos, function ($r) use (&$seen) {
if (isset($seen[$r['name']])) {
return false;
}
$seen[$r['name']] = true;
return true;
});
} else {
$reposArg = $this->getArgument('--repos', '');
if (empty($reposArg)) {
$this->log("❌ --repos or --all required for --sync-updates", 'ERROR');
return 1;
}
$names = array_filter(array_map('trim', explode(',', $reposArg)));
foreach ($names as $name) {
$repos[] = ['name' => $name];
}
}
if (empty($repos)) {
$this->log("No Joomla repositories to sync updates for", 'INFO');
return 0;
}
// Create both platform adapters
try {
$adapters = PlatformAdapterFactory::createBoth($this->config);
} catch (\Exception $e) {
$this->log("❌ Both platform tokens required for --sync-updates: " . $e->getMessage(), 'ERROR');
return 1;
}
$gitea = $adapters['gitea'];
$github = $adapters['github'];
$dryRun = $this->dryRun;
$this->log("\nSyncing updates.xml across Gitea <-> GitHub for " . count($repos) . " repo(s)...", 'INFO');
$synced = 0;
$failed = 0;
foreach ($repos as $repo) {
$name = $repo['name'];
$this->log("\n[{$name}]", 'INFO');
// Try both updates.xml and updates.xml filenames
$updateFile = $this->resolveUpdateFile($gitea, $github, $org, $name);
if ($updateFile === null) {
$this->log(" ⊘ No update(s).xml found on either platform", 'INFO');
continue;
}
$fileName = $updateFile['name'];
$source = $updateFile['source']; // 'gitea' or 'github'
$content = $updateFile['content'];
$target = $source === 'gitea' ? 'github' : 'gitea';
$targetAdapter = $source === 'gitea' ? $github : $gitea;
$this->log(" Source: {$source} ({$fileName})", 'INFO');
if ($dryRun) {
$this->log(" (dry-run) Would push {$fileName} to {$target}", 'INFO');
$synced++;
continue;
}
// Push to the other platform
try {
$existingSha = null;
try {
$existing = $targetAdapter->getFileContents($org, $name, $fileName);
$existingSha = $existing['sha'] ?? null;
// Compare content — skip if identical
$existingContent = base64_decode($existing['content'] ?? '');
if (trim($existingContent) === trim($content)) {
$this->log(" ✓ Already in sync", 'INFO');
$synced++;
continue;
}
} catch (\Exception $e) {
$targetAdapter->getApiClient()->resetCircuitBreaker();
}
$targetAdapter->createOrUpdateFile(
$org,
$name,
$fileName,
$content,
"chore: sync {$fileName} from {$source}",
$existingSha
);
$this->log(" ✓ Pushed to {$target}", 'INFO');
$synced++;
} catch (\Exception $e) {
$this->log(" ✗ Failed to push to {$target}: " . $e->getMessage(), 'ERROR');
$targetAdapter->getApiClient()->resetCircuitBreaker();
$failed++;
}
}
$this->log("\n" . str_repeat('=', 50), 'INFO');
$this->log("Updates sync complete: {$synced} synced, {$failed} failed", 'INFO');
return $failed > 0 ? 1 : 0;
}
/**
* Find the updates file on both platforms, return the one with the higher version.
*
* Checks both `updates.xml` and `updates.xml` filenames.
* Returns the content from the platform with the newer <version>.
* Gitea wins ties (primary platform).
*
* @return array{name: string, source: string, content: string}|null
*/
private function resolveUpdateFile(
GitPlatformAdapter $gitea,
GitPlatformAdapter $github,
string $org,
string $name
): ?array {
$candidates = ['updates.xml', 'updates.xml'];
$found = []; // platform => [name, content, version]
foreach (['gitea' => $gitea, 'github' => $github] as $platform => $adapter) {
foreach ($candidates as $fileName) {
try {
$file = $adapter->getFileContents($org, $name, $fileName);
$content = base64_decode($file['content'] ?? '');
// Extract latest version from the XML
$version = '0.0.0';
if (preg_match('/<version>([^<]+)<\/version>/', $content, $m)) {
$version = trim($m[1]);
}
$found[$platform] = [
'name' => $fileName,
'content' => $content,
'version' => $version,
];
break; // Found one — stop checking other filenames for this platform
} catch (\Exception $e) {
$adapter->getApiClient()->resetCircuitBreaker();
}
}
}
if (empty($found)) {
return null;
}
// If only one platform has it, that's the source
if (count($found) === 1) {
$platform = array_key_first($found);
return [
'name' => $found[$platform]['name'],
'source' => $platform,
'content' => $found[$platform]['content'],
];
}
// Both have it — pick the one with the higher version (Gitea wins ties)
$giteaVer = $found['gitea']['version'];
$githubVer = $found['github']['version'];
$source = version_compare($githubVer, $giteaVer, '>') ? 'github' : 'gitea';
return [
'name' => $found[$source]['name'],
'source' => $source,
'content' => $found[$source]['content'],
];
}
}
// Execute if run directly
+4 -4
View File
@@ -574,12 +574,12 @@ class BulkSync extends CliFramework
*/
private function writeStepSummary(array $results): void
{
// Check both GitHub and Gitea step summary env vars
// Check both GitHub and MokoGit step summary env vars
$summaryFile = getenv($this->adapter->getStepSummaryEnvVar());
if (empty($summaryFile)) {
// Fallback: also check the other platform's env var
$fallback = $this->adapter->getPlatformName() === 'github'
? getenv('GITEA_STEP_SUMMARY')
? getenv('MOKOGIT_STEP_SUMMARY')
: getenv('GITHUB_STEP_SUMMARY');
$summaryFile = $fallback ?: '';
}
@@ -719,7 +719,7 @@ class BulkSync extends CliFramework
$score = 0;
$max = 0;
// 1. Check branch protection rules (rulesets on GitHub, branch_protections on Gitea)
// 1. Check branch protection rules (rulesets on GitHub, branch_protections on MokoGit)
$max += 20;
try {
$protections = $this->adapter->listBranchProtections($org, $name);
@@ -1080,7 +1080,7 @@ class BulkSync extends CliFramework
* trail of what was changed and why.
*/
/**
* Resolve label names to their integer IDs for the Gitea API.
* Resolve label names to their integer IDs for the MokoGit API.
* Creates missing labels automatically.
*
* @param string $org Organization name
+8 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Trigger a workflow across all client-waas repos in a Gitea org
# BRIEF: Trigger a workflow across all client-waas repos in a MokoGit org
set -euo pipefail
@@ -10,11 +10,11 @@ set -euo pipefail
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") GITEA_URL TOKEN ORG WORKFLOW [REF] [INPUTS]
Usage: $(basename "$0") MOKOGIT_URL TOKEN ORG WORKFLOW [REF] [INPUTS]
Arguments:
GITEA_URL Base URL of the Gitea instance (e.g. https://git.mokoconsulting.tech)
TOKEN Gitea API token with repo/action permissions
MOKOGIT_URL Base URL of the MokoGit instance (e.g. https://git.mokoconsulting.tech)
TOKEN MokoGit API token with repo/action permissions
ORG Organisation or user that owns the repos
WORKFLOW Workflow filename to trigger (e.g. dependency-audit.yml)
REF Branch ref to run against (default: main)
@@ -33,7 +33,7 @@ if [ $# -lt 4 ]; then
usage
fi
GITEA_URL="${1%/}"
MOKOGIT_URL="${1%/}"
TOKEN="$2"
ORG="$3"
WORKFLOW="$4"
@@ -43,7 +43,7 @@ INPUTS="${6:-{\}}"
# ---------------------------------------------------------------------------
# Fetch all repos in the org, paginated
# ---------------------------------------------------------------------------
echo "Fetching repos for org '${ORG}' on ${GITEA_URL} ..."
echo "Fetching repos for org '${ORG}' on ${MOKOGIT_URL} ..."
PAGE=1
LIMIT=50
@@ -53,7 +53,7 @@ while true; do
RESPONSE=$(curl -s \
-H "Authorization: token ${TOKEN}" \
-H "Accept: application/json" \
"${GITEA_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=${LIMIT}")
"${MOKOGIT_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=${LIMIT}")
# Break if empty array
COUNT=$(echo "$RESPONSE" | jq -r 'length')
@@ -101,7 +101,7 @@ while IFS= read -r REPO; do
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${GITEA_URL}/api/v1/repos/${ORG}/${REPO}/actions/workflows/${WORKFLOW}/dispatches")
"${MOKOGIT_URL}/api/v1/repos/${ORG}/${REPO}/actions/workflows/${WORKFLOW}/dispatches")
if [ "$HTTP_CODE" -eq 204 ] || [ "$HTTP_CODE" -eq 201 ]; then
echo " [OK] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
+20 -20
View File
@@ -10,7 +10,7 @@
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# BRIEF: Creates or updates a MokoGit issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
@@ -19,10 +19,10 @@
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
MOKOGIT_URL="${MOKOGIT_URL:-https://git.mokoconsulting.tech}"
MOKOGIT_TOKEN="${MOKOGIT_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
RUN_URL="${GITHUB_SERVER_URL:-${MOKOGIT_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
@@ -45,8 +45,8 @@ Optional:
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
--token MokoGit API token (default: \$MOKOGIT_TOKEN)
--url MokoGit base URL (default: \$MOKOGIT_URL)
EOF
exit 1
}
@@ -59,8 +59,8 @@ while [[ $# -gt 0 ]]; do
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
--token) MOKOGIT_TOKEN="$2"; shift 2 ;;
--url) MOKOGIT_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
@@ -68,10 +68,10 @@ done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$MOKOGIT_TOKEN" ]] && { echo "ERROR: MOKOGIT_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
API="${MOKOGIT_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
@@ -84,20 +84,20 @@ fi
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
@@ -114,7 +114,7 @@ find_existing_issue() {
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
@@ -151,7 +151,7 @@ ${DETAILS}
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
*Auto-created by [ci-issue-reporter](${MOKOGIT_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
@@ -181,7 +181,7 @@ import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
@@ -205,7 +205,7 @@ print(json.dumps({
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
@@ -213,16 +213,16 @@ print(json.dumps({
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
# Apply label (separate call — more reliable across MokoGit versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
+3 -3
View File
@@ -12,8 +12,8 @@
# =============================================================================
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
ORG="${GITEA_ORG:-MokoConsulting}"
MOKOGIT_URL="${MOKOGIT_URL:-https://git.mokoconsulting.tech}"
ORG="${MOKOGIT_ORG:-MokoConsulting}"
TOKEN="${GA_TOKEN:?GA_TOKEN required}"
DRY_RUN=false
FILTER_REPOS=""
@@ -32,7 +32,7 @@ api() {
local method="$1" path="$2" data="${3:-}"
local args=(-sf -H "Authorization: token $TOKEN" -H "Content-Type: application/json" -X "$method")
[[ -n "$data" ]] && args+=(-d "$data")
curl "${args[@]}" "$GITEA_URL/api/v1$path" 2>/dev/null
curl "${args[@]}" "$MOKOGIT_URL/api/v1$path" 2>/dev/null
}
# Get repos
-481
View File
@@ -1,481 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/enrich_manifest_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
*
* Note: This script uses proc_open for shell commands. All arguments are escaped
* via escapeshellarg(). No user-supplied input reaches the shell unescaped.
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
use MokoCli\ManifestParser;
class EnrichManifestXmlCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Enrich XML manifests with repo-specific build and deploy details');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
echo "=== mokocli XML Manifest Enrichment ===\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) {
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['enriched' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " {$name} ... SKIP (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
$stats['skipped']++;
continue;
}
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} ... ";
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch)
. ' ' . escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
$stats['failed']++;
continue;
}
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokocli')) {
echo "SKIP (no XML manifest)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
$existingXml = file_get_contents($manifestPath);
$platform = $parser->extractPlatform($existingXml) ?? 'default-repository';
$enrichment = $this->inspectRepo($workDir, $platform);
if (!isset($enrichment['build'])) {
$enrichment['build'] = [];
}
$enrichment['build']['language'] = $enrichment['build']['language']
?? $repo['language']
?? ManifestParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform);
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []);
$sc = count($enrichment['scripts'] ?? []);
$details = "deploy={$dc} scripts={$sc}";
if ($this->dryRun) {
echo "WOULD ENRICH [{$details}]\n";
$stats['enriched']++;
$this->rmTree($workDir);
continue;
}
file_put_contents($manifestPath, $enrichedXml);
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
[$cr, $co] = $this->gitCmd($workDir, 'commit', '-m', "chore: enrich manifest.xml with build/deploy/scripts\n\nAuto-detected: {$details}");
if ($cr !== 0) {
echo "SKIP (no diff)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
[$pr] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pr !== 0) {
echo "FAIL (push)\n";
$stats['failed']++;
} else {
echo "ENRICHED [{$details}]\n";
$stats['enriched']++;
}
$this->rmTree($workDir);
}
@rmdir($tmpBase);
echo "\n=== Summary ===\nEnriched: {$stats['enriched']}\nSkipped: {$stats['skipped']}\nFailed: {$stats['failed']}\n";
return 0;
}
private function inspectRepo(string $workDir, string $platform): array
{
$enrichment = [];
$build = [];
// Detect entry point
if (is_dir("{$workDir}/src")) {
foreach (glob("{$workDir}/src/*.xml") ?: [] as $xf) {
$c = file_get_contents($xf);
if (str_contains($c, '<extension') || str_contains($c, '<install')) {
$build['entry_point'] = 'src/' . basename($xf);
break;
}
}
foreach (glob("{$workDir}/src/core/modules/mod*.class.php") ?: [] as $mf) {
$build['entry_point'] = str_replace("{$workDir}/", '', $mf);
break;
}
}
// composer.json
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$phpReq = $composer['require']['php'] ?? null;
if ($phpReq) {
$build['runtime'] = "php:{$phpReq}";
}
$deps = [];
foreach (['joomla/cms', 'joomla/framework', 'dolibarr/dolibarr'] as $pd) {
if (isset($composer['require'][$pd])) {
$deps[] = ['name' => $pd, 'version' => $composer['require'][$pd], 'type' => 'platform'];
}
}
if (isset($composer['require']['mokoconsulting-tech/enterprise'])) {
$deps[] = [
'name' => 'mokoconsulting-tech/enterprise',
'version' => $composer['require']['mokoconsulting-tech/enterprise'],
'type' => 'composer',
];
}
if (!empty($deps)) {
$build['dependencies'] = $deps;
}
}
// Artifact from Makefile
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
if (preg_match('/\bdist\/(\S+\.zip)\b/', $mk, $m)) {
$build['artifact'] = ['format' => 'zip', 'path' => 'dist/', 'filename' => $m[1]];
}
}
if (!empty($build)) {
$enrichment['build'] = $build;
}
// Deploy targets from workflows
$targets = [];
$wfDir = is_dir("{$workDir}/.gitea/workflows") ? "{$workDir}/.gitea/workflows" : "{$workDir}/.github/workflows";
if (is_dir($wfDir)) {
foreach (['deploy-dev', 'deploy-demo', 'deploy-rs'] as $dn) {
$wf = "{$wfDir}/{$dn}.yml";
if (!file_exists($wf)) {
continue;
}
$wc = file_get_contents($wf);
$t = ['name' => str_replace('deploy-', '', $dn)];
if (str_contains($wc, 'sftp') || str_contains($wc, 'SFTP')) {
$t['method'] = 'sftp';
} elseif (str_contains($wc, 'rsync')) {
$t['method'] = 'rsync';
}
if (str_contains($wc, 'src/')) {
$t['src_dir'] = 'src/';
}
if (preg_match('/branches:\s*\n\s*-\s*["\']?([^"\'}\s]+)/', $wc, $m)) {
$t['branch'] = $m[1];
}
$targets[] = $t;
}
}
if (!empty($targets)) {
$enrichment['deploy'] = $targets;
}
// Scripts from Makefile + composer
$scripts = [];
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
$known = [
'build' => 'build', 'test' => 'test', 'lint' => 'lint',
'clean' => 'build', 'package' => 'build',
'validate' => 'validate', 'release' => 'release',
];
if (preg_match_all('/^([a-zA-Z_-]+)\s*:/m', $mk, $matches)) {
foreach ($matches[1] as $tgt) {
$tl = strtolower($tgt);
if (isset($known[$tl])) {
$scripts[] = [
'name' => $tl, 'phase' => $known[$tl],
'command' => "make {$tgt}",
'desc' => ucfirst($tl) . ' via make',
'runner' => 'make',
];
}
}
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$km = ['test' => 'test','lint' => 'lint','cs' => 'lint','phpcs' => 'lint','phpstan' => 'lint','validate' => 'validate'];
foreach ($composer['scripts'] ?? [] as $sn => $cmd) {
$sl = strtolower($sn);
foreach ($km as $match => $phase) {
if (str_contains($sl, $match)) {
$exists = false;
foreach ($scripts as $s) {
if ($s['name'] === $sl) {
$exists = true;
break;
}
}
if (!$exists) {
$scripts[] = [
'name' => $sn, 'phase' => $phase,
'command' => "composer run {$sn}",
'desc' => is_string($cmd) ? $cmd : "Run {$sn}",
'runner' => 'composer',
];
}
break;
}
}
}
}
if (!empty($scripts)) {
$enrichment['scripts'] = $scripts;
}
return $enrichment;
}
private function enrichManifestXml(string $xml, array $enrichment): string
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (!$dom->loadXML($xml)) {
return $xml;
}
$ns = ManifestParser::NAMESPACE_URI;
$root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) {
$toRemove = [];
$existing = $root->getElementsByTagNameNS($ns, $tag);
for ($i = 0; $i < $existing->length; $i++) {
$toRemove[] = $existing->item($i);
}
foreach ($toRemove as $node) {
$root->removeChild($node);
}
}
if (!empty($enrichment['build'])) {
$buildEl = $dom->createElementNS($ns, 'build');
$b = $enrichment['build'];
foreach (['language', 'runtime'] as $f) {
if (isset($b[$f])) {
$buildEl->appendChild($dom->createElementNS($ns, $f, htmlspecialchars($b[$f], ENT_XML1)));
}
}
if (isset($b['package_type'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'package-type', htmlspecialchars($b['package_type'], ENT_XML1)));
}
if (isset($b['entry_point'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'entry-point', htmlspecialchars($b['entry_point'], ENT_XML1)));
}
if (isset($b['artifact'])) {
$art = $dom->createElementNS($ns, 'artifact');
foreach (['format','path','filename'] as $af) {
if (isset($b['artifact'][$af])) {
$art->appendChild($dom->createElementNS($ns, $af, htmlspecialchars($b['artifact'][$af], ENT_XML1)));
}
}
$buildEl->appendChild($art);
}
if (isset($b['dependencies'])) {
$deps = $dom->createElementNS($ns, 'dependencies');
foreach ($b['dependencies'] as $d) {
$req = $dom->createElementNS($ns, 'requires', '');
$req->setAttribute('name', $d['name']);
if (isset($d['version'])) {
$req->setAttribute('version', $d['version']);
}
if (isset($d['type'])) {
$req->setAttribute('type', $d['type']);
}
$deps->appendChild($req);
}
$buildEl->appendChild($deps);
}
$root->appendChild($buildEl);
}
if (!empty($enrichment['deploy'])) {
$deploy = $dom->createElementNS($ns, 'deploy');
foreach ($enrichment['deploy'] as $t) {
$target = $dom->createElementNS($ns, 'target');
$target->setAttribute('name', $t['name']);
$target->appendChild($dom->createElementNS($ns, 'host', '${{ secrets.' . strtoupper($t['name']) . '_HOST }}'));
$target->appendChild($dom->createElementNS($ns, 'path', '${{ secrets.' . strtoupper($t['name']) . '_PATH }}'));
if (isset($t['method'])) {
$target->appendChild($dom->createElementNS($ns, 'method', $t['method']));
}
if (isset($t['branch'])) {
$target->appendChild($dom->createElementNS($ns, 'branch', htmlspecialchars($t['branch'], ENT_XML1)));
}
if (isset($t['src_dir'])) {
$target->appendChild($dom->createElementNS($ns, 'src-dir', htmlspecialchars($t['src_dir'], ENT_XML1)));
}
$deploy->appendChild($target);
}
$root->appendChild($deploy);
}
if (!empty($enrichment['scripts'])) {
$scriptsEl = $dom->createElementNS($ns, 'scripts');
foreach ($enrichment['scripts'] as $s) {
$script = $dom->createElementNS($ns, 'script');
$script->setAttribute('name', $s['name']);
if (isset($s['phase'])) {
$script->setAttribute('phase', $s['phase']);
}
$script->appendChild($dom->createElementNS($ns, 'command', htmlspecialchars($s['command'], ENT_XML1)));
if (isset($s['desc'])) {
$script->appendChild($dom->createElementNS($ns, 'description', htmlspecialchars($s['desc'], ENT_XML1)));
}
if (isset($s['runner'])) {
$script->appendChild($dom->createElementNS($ns, 'runner', htmlspecialchars($s['runner'], ENT_XML1)));
}
$scriptsEl->appendChild($script);
}
$root->appendChild($scriptsEl);
}
return $dom->saveXML();
}
/** @return array{int, string} */
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd);
if (!is_resource($proc)) {
return [1, "proc_open failed"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return [proc_close($proc), trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/** @return array{int, string} */
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_TIMEOUT => 30]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new EnrichManifestXmlCli();
exit($app->execute());
-484
View File
@@ -1,484 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/enrich_mokostandards_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
*
* Note: This script uses proc_open for shell commands. All arguments are escaped
* via escapeshellarg(). No user-supplied input reaches the shell unescaped.
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
use MokoCli\ManifestParser;
class EnrichMokostandardsXmlCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Enrich XML manifests with repo-specific build and deploy details');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
echo "=== mokocli XML Manifest Enrichment ===\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) {
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['enriched' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " {$name} ... SKIP (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
$stats['skipped']++;
continue;
}
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} ... ";
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch)
. ' ' . escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
$stats['failed']++;
continue;
}
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokocli')) {
echo "SKIP (no XML manifest)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
$existingXml = file_get_contents($manifestPath);
$platform = $parser->extractPlatform($existingXml) ?? 'default-repository';
$enrichment = $this->inspectRepo($workDir, $platform);
if (!isset($enrichment['build'])) {
$enrichment['build'] = [];
}
$enrichment['build']['language'] = $enrichment['build']['language']
?? $repo['language']
?? ManifestParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform);
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []);
$sc = count($enrichment['scripts'] ?? []);
$details = "deploy={$dc} scripts={$sc}";
if ($this->dryRun) {
echo "WOULD ENRICH [{$details}]\n";
$stats['enriched']++;
$this->rmTree($workDir);
continue;
}
file_put_contents($manifestPath, $enrichedXml);
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
$commitMsg = "chore: enrich .mokostandards"
. " with build/deploy/scripts\n\n"
. "Auto-detected: {$details}";
[$cr, $co] = $this->gitCmd(
$workDir,
'commit',
'-m',
$commitMsg
);
if ($cr !== 0) {
echo "SKIP (no diff)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
[$pr] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pr !== 0) {
echo "FAIL (push)\n";
$stats['failed']++;
} else {
echo "ENRICHED [{$details}]\n";
$stats['enriched']++;
}
$this->rmTree($workDir);
}
@rmdir($tmpBase);
echo "\n=== Summary ===\nEnriched: {$stats['enriched']}\nSkipped: {$stats['skipped']}\nFailed: {$stats['failed']}\n";
return 0;
}
private function inspectRepo(string $workDir, string $platform): array
{
$enrichment = [];
$build = [];
if (is_dir("{$workDir}/src")) {
foreach (glob("{$workDir}/src/*.xml") ?: [] as $xf) {
$c = file_get_contents($xf);
if (str_contains($c, '<extension') || str_contains($c, '<install')) {
$build['entry_point'] = 'src/' . basename($xf);
break;
}
}
foreach (glob("{$workDir}/src/core/modules/mod*.class.php") ?: [] as $mf) {
$build['entry_point'] = str_replace("{$workDir}/", '', $mf);
break;
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$phpReq = $composer['require']['php'] ?? null;
if ($phpReq) {
$build['runtime'] = "php:{$phpReq}";
}
$deps = [];
foreach (['joomla/cms', 'joomla/framework', 'dolibarr/dolibarr'] as $pd) {
if (isset($composer['require'][$pd])) {
$deps[] = ['name' => $pd, 'version' => $composer['require'][$pd], 'type' => 'platform'];
}
}
if (isset($composer['require']['mokoconsulting-tech/enterprise'])) {
$deps[] = [
'name' => 'mokoconsulting-tech/enterprise',
'version' => $composer['require']['mokoconsulting-tech/enterprise'],
'type' => 'composer',
];
}
if (!empty($deps)) {
$build['dependencies'] = $deps;
}
}
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
if (preg_match('/\bdist\/(\S+\.zip)\b/', $mk, $m)) {
$build['artifact'] = ['format' => 'zip', 'path' => 'dist/', 'filename' => $m[1]];
}
}
if (!empty($build)) {
$enrichment['build'] = $build;
}
$targets = [];
$wfDir = is_dir("{$workDir}/.gitea/workflows") ? "{$workDir}/.gitea/workflows" : "{$workDir}/.github/workflows";
if (is_dir($wfDir)) {
foreach (['deploy-dev', 'deploy-demo', 'deploy-rs'] as $dn) {
$wf = "{$wfDir}/{$dn}.yml";
if (!file_exists($wf)) {
continue;
}
$wc = file_get_contents($wf);
$t = ['name' => str_replace('deploy-', '', $dn)];
if (str_contains($wc, 'sftp') || str_contains($wc, 'SFTP')) {
$t['method'] = 'sftp';
} elseif (str_contains($wc, 'rsync')) {
$t['method'] = 'rsync';
}
if (str_contains($wc, 'src/')) {
$t['src_dir'] = 'src/';
}
if (preg_match('/branches:\s*\n\s*-\s*["\']?([^"\'}\s]+)/', $wc, $m)) {
$t['branch'] = $m[1];
}
$targets[] = $t;
}
}
if (!empty($targets)) {
$enrichment['deploy'] = $targets;
}
$scripts = [];
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
$known = [
'build' => 'build', 'test' => 'test', 'lint' => 'lint',
'clean' => 'build', 'package' => 'build',
'validate' => 'validate', 'release' => 'release',
];
if (preg_match_all('/^([a-zA-Z_-]+)\s*:/m', $mk, $matches)) {
foreach ($matches[1] as $tgt) {
$tl = strtolower($tgt);
if (isset($known[$tl])) {
$scripts[] = [
'name' => $tl, 'phase' => $known[$tl],
'command' => "make {$tgt}",
'desc' => ucfirst($tl) . ' via make',
'runner' => 'make',
];
}
}
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$km = ['test' => 'test','lint' => 'lint','cs' => 'lint','phpcs' => 'lint','phpstan' => 'lint','validate' => 'validate'];
foreach ($composer['scripts'] ?? [] as $sn => $cmd) {
$sl = strtolower($sn);
foreach ($km as $match => $phase) {
if (str_contains($sl, $match)) {
$exists = false;
foreach ($scripts as $s) {
if ($s['name'] === $sl) {
$exists = true;
break;
}
}
if (!$exists) {
$scripts[] = [
'name' => $sn, 'phase' => $phase,
'command' => "composer run {$sn}",
'desc' => is_string($cmd) ? $cmd : "Run {$sn}",
'runner' => 'composer',
];
}
break;
}
}
}
}
if (!empty($scripts)) {
$enrichment['scripts'] = $scripts;
}
return $enrichment;
}
private function enrichManifestXml(string $xml, array $enrichment): string
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (!$dom->loadXML($xml)) {
return $xml;
}
$ns = ManifestParser::NAMESPACE_URI;
$root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) {
$toRemove = [];
$existing = $root->getElementsByTagNameNS($ns, $tag);
for ($i = 0; $i < $existing->length; $i++) {
$toRemove[] = $existing->item($i);
}
foreach ($toRemove as $node) {
$root->removeChild($node);
}
}
if (!empty($enrichment['build'])) {
$buildEl = $dom->createElementNS($ns, 'build');
$b = $enrichment['build'];
foreach (['language', 'runtime'] as $f) {
if (isset($b[$f])) {
$buildEl->appendChild($dom->createElementNS($ns, $f, htmlspecialchars($b[$f], ENT_XML1)));
}
}
if (isset($b['package_type'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'package-type', htmlspecialchars($b['package_type'], ENT_XML1)));
}
if (isset($b['entry_point'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'entry-point', htmlspecialchars($b['entry_point'], ENT_XML1)));
}
if (isset($b['artifact'])) {
$art = $dom->createElementNS($ns, 'artifact');
foreach (['format','path','filename'] as $af) {
if (isset($b['artifact'][$af])) {
$art->appendChild($dom->createElementNS($ns, $af, htmlspecialchars($b['artifact'][$af], ENT_XML1)));
}
}
$buildEl->appendChild($art);
}
if (isset($b['dependencies'])) {
$deps = $dom->createElementNS($ns, 'dependencies');
foreach ($b['dependencies'] as $d) {
$req = $dom->createElementNS($ns, 'requires', '');
$req->setAttribute('name', $d['name']);
if (isset($d['version'])) {
$req->setAttribute('version', $d['version']);
}
if (isset($d['type'])) {
$req->setAttribute('type', $d['type']);
}
$deps->appendChild($req);
}
$buildEl->appendChild($deps);
}
$root->appendChild($buildEl);
}
if (!empty($enrichment['deploy'])) {
$deploy = $dom->createElementNS($ns, 'deploy');
foreach ($enrichment['deploy'] as $t) {
$target = $dom->createElementNS($ns, 'target');
$target->setAttribute('name', $t['name']);
$target->appendChild($dom->createElementNS($ns, 'host', '${{ secrets.' . strtoupper($t['name']) . '_HOST }}'));
$target->appendChild($dom->createElementNS($ns, 'path', '${{ secrets.' . strtoupper($t['name']) . '_PATH }}'));
if (isset($t['method'])) {
$target->appendChild($dom->createElementNS($ns, 'method', $t['method']));
}
if (isset($t['branch'])) {
$target->appendChild($dom->createElementNS($ns, 'branch', htmlspecialchars($t['branch'], ENT_XML1)));
}
if (isset($t['src_dir'])) {
$target->appendChild($dom->createElementNS($ns, 'src-dir', htmlspecialchars($t['src_dir'], ENT_XML1)));
}
$deploy->appendChild($target);
}
$root->appendChild($deploy);
}
if (!empty($enrichment['scripts'])) {
$scriptsEl = $dom->createElementNS($ns, 'scripts');
foreach ($enrichment['scripts'] as $s) {
$script = $dom->createElementNS($ns, 'script');
$script->setAttribute('name', $s['name']);
if (isset($s['phase'])) {
$script->setAttribute('phase', $s['phase']);
}
$script->appendChild($dom->createElementNS($ns, 'command', htmlspecialchars($s['command'], ENT_XML1)));
if (isset($s['desc'])) {
$script->appendChild($dom->createElementNS($ns, 'description', htmlspecialchars($s['desc'], ENT_XML1)));
}
if (isset($s['runner'])) {
$script->appendChild($dom->createElementNS($ns, 'runner', htmlspecialchars($s['runner'], ENT_XML1)));
}
$scriptsEl->appendChild($script);
}
$root->appendChild($scriptsEl);
}
return $dom->saveXML();
}
/** @return array{int, string} */
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd);
if (!is_resource($proc)) {
return [1, "proc_open failed"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return [proc_close($proc), trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/** @return array{int, string} */
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_TIMEOUT => 30]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new EnrichMokostandardsXmlCli();
exit($app->execute());
@@ -11,14 +11,14 @@
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/migrate_to_gitea.php
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
* PATH: /automation/migrate_to_mokogit.php
* BRIEF: Migrate repositories from GitHub to self-hosted MokoGit instance
*
* USAGE
* php automation/migrate_to_gitea.php --dry-run
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
* php automation/migrate_to_gitea.php --exclude mokocli --skip-archived
* php automation/migrate_to_gitea.php --resume
* php automation/migrate_to_mokogit.php --dry-run
* php automation/migrate_to_mokogit.php --repos MokoCRM MokoDoliMods
* php automation/migrate_to_mokogit.php --exclude mokocli --skip-archived
* php automation/migrate_to_mokogit.php --resume
*/
declare(strict_types=1);
@@ -30,32 +30,32 @@ use MokoCli\CliFramework;
use MokoCli\Config;
use MokoCli\PlatformAdapterFactory;
use MokoCli\GitHubAdapter;
use MokoCli\MokoGiteaAdapter;
use MokoCli\MokoGitAdapter;
/**
* Gitea Migration Script
* MokoGit Migration Script
*
* Migrates repositories from GitHub to a self-hosted Gitea instance.
* Uses Gitea's built-in migration endpoint for git history, tags, releases,
* Migrates repositories from GitHub to a self-hosted MokoGit instance.
* Uses MokoGit's built-in migration endpoint for git history, tags, releases,
* issues, and labels. Post-migration applies branch protection, topics,
* and workflow conversion.
*/
class MigrateToGitea extends CliFramework
class MigrateToMokoGit extends CliFramework
{
private ?GitHubAdapter $github = null;
private ?MokoGiteaAdapter $gitea = null;
private ?MokoGitAdapter $mokogit = null;
private ?CheckpointManager $checkpoints = null;
protected function configure(): void
{
$this->setDescription('Migrate repositories from GitHub to Gitea');
$this->setDescription('Migrate repositories from GitHub to MokoGit');
$this->addArgument('--dry-run', 'Show what would be migrated without making changes', false);
$this->addArgument('--repos', 'Specific repositories to migrate (space-separated)', '');
$this->addArgument('--exclude', 'Repositories to exclude (space-separated)', '');
$this->addArgument('--skip-archived', 'Skip archived repositories', false);
$this->addArgument('--resume', 'Resume from last checkpoint', false);
$this->addArgument('--github-token', 'GitHub token override', '');
$this->addArgument('--gitea-token', 'Gitea token override', '');
$this->addArgument('--mokogit-token', 'MokoGit token override', '');
}
protected function run(): int
@@ -70,19 +70,19 @@ class MigrateToGitea extends CliFramework
// Override tokens if provided
$ghToken = (string) $this->getArgument('--github-token');
$giteaToken = (string) $this->getArgument('--gitea-token');
$mokogitToken = (string) $this->getArgument('--mokogit-token');
if ($ghToken !== '') {
$config->set('github.token', $ghToken);
}
if ($giteaToken !== '') {
$config->set('gitea.token', $giteaToken);
if ($mokogitToken !== '') {
$config->set('mokogit.token', $mokogitToken);
}
// Create both adapters
try {
$adapters = PlatformAdapterFactory::createBoth($config);
$this->github = $adapters['github'];
$this->gitea = $adapters['gitea'];
$this->mokogit = $adapters['mokogit'];
} catch (\RuntimeException $e) {
$this->log('ERROR', $e->getMessage());
return 1;
@@ -90,11 +90,11 @@ class MigrateToGitea extends CliFramework
$this->checkpoints = new CheckpointManager('.checkpoints/migration');
$org = $config->getString('github.organization', 'MokoConsulting');
$giteaOrg = $config->getString('gitea.organization', 'MokoConsulting');
$mokogitOrg = $config->getString('mokogit.organization', 'MokoConsulting');
echo "=== Gitea Migration Tool ===\n";
echo "=== MokoGit Migration Tool ===\n";
echo "Source: GitHub ({$org})\n";
echo "Destination: Gitea ({$giteaOrg}) at " . $config->getString('gitea.url') . "\n";
echo "Destination: MokoGit ({$mokogitOrg}) at " . $config->getString('mokogit.url') . "\n";
echo "Mode: " . ($dryRun ? 'DRY RUN' : 'LIVE') . "\n\n";
// ── Phase 1: Discovery ──────────────────────────────────────────
@@ -111,22 +111,22 @@ class MigrateToGitea extends CliFramework
$ghRepos = array_filter($ghRepos, fn($r) => !in_array($r['name'], $excludeRepos, true));
}
// Check which already exist on Gitea
$giteaRepos = [];
// Check which already exist on MokoGit
$mokogitRepos = [];
try {
$existing = $this->gitea->listOrgRepos($giteaOrg);
$existing = $this->mokogit->listOrgRepos($mokogitOrg);
foreach ($existing as $r) {
$giteaRepos[$r['name']] = true;
$mokogitRepos[$r['name']] = true;
}
} catch (\Exception $e) {
echo "Note: Could not list Gitea repos (org may not exist yet): {$e->getMessage()}\n";
echo "Note: Could not list MokoGit repos (org may not exist yet): {$e->getMessage()}\n";
}
$toMigrate = [];
$toSkip = [];
foreach ($ghRepos as $repo) {
$name = $repo['name'];
if (isset($giteaRepos[$name])) {
if (isset($mokogitRepos[$name])) {
$toSkip[] = $name;
} else {
$toMigrate[] = $repo;
@@ -135,7 +135,7 @@ class MigrateToGitea extends CliFramework
echo "\nMigration plan:\n";
echo " Migrate: " . count($toMigrate) . " repositories\n";
echo " Skip: " . count($toSkip) . " (already on Gitea)\n";
echo " Skip: " . count($toSkip) . " (already on MokoGit)\n";
if (!empty($toSkip)) {
echo " Skipped: " . implode(', ', $toSkip) . "\n";
}
@@ -163,7 +163,7 @@ class MigrateToGitea extends CliFramework
$results = ['migrated' => [], 'failed' => [], 'skipped' => $toSkip];
// Resume support
$checkpoint = $resume ? $this->checkpoints->loadCheckpoint('gitea_migration') : null;
$checkpoint = $resume ? $this->checkpoints->loadCheckpoint('mokogit_migration') : null;
$startFrom = $checkpoint['last_completed'] ?? '';
$skipUntil = !empty($startFrom);
@@ -182,11 +182,11 @@ class MigrateToGitea extends CliFramework
try {
// Shallow migration — copy current branch state only, no past
// commit history. This gives every repo a clean start on Gitea.
$this->gitea->migrateRepository([
// commit history. This gives every repo a clean start on MokoGit.
$this->mokogit->migrateRepository([
'clone_addr' => "https://github.com/{$org}/{$name}.git",
'repo_name' => $name,
'repo_owner' => $giteaOrg,
'repo_owner' => $mokogitOrg,
'service' => 'github',
'auth_token' => $ghToken,
'mirror' => false,
@@ -203,7 +203,7 @@ class MigrateToGitea extends CliFramework
$results['migrated'][] = $name;
// Save checkpoint after each successful migration
$this->checkpoints->saveCheckpoint('gitea_migration', [
$this->checkpoints->saveCheckpoint('mokogit_migration', [
'last_completed' => $name,
'migrated' => $results['migrated'],
'failed' => $results['failed'],
@@ -211,7 +211,7 @@ class MigrateToGitea extends CliFramework
} catch (\Exception $e) {
echo " FAILED: " . $e->getMessage() . "\n";
$results['failed'][] = ['name' => $name, 'error' => $e->getMessage()];
$this->gitea->getApiClient()->resetCircuitBreaker();
$this->mokogit->getApiClient()->resetCircuitBreaker();
}
}
@@ -225,12 +225,12 @@ class MigrateToGitea extends CliFramework
// Apply topics from GitHub
$ghTopics = $this->github->getRepoTopics($org, $name);
if (!empty($ghTopics)) {
$this->gitea->setRepoTopics($giteaOrg, $name, $ghTopics);
$this->mokogit->setRepoTopics($mokogitOrg, $name, $ghTopics);
echo " Topics applied\n";
}
// Apply branch protection
$this->gitea->setBranchProtection($giteaOrg, $name, 'main', [
$this->mokogit->setBranchProtection($mokogitOrg, $name, 'main', [
'required_reviews' => 1,
'dismiss_stale' => true,
'block_on_rejected' => true,
@@ -238,7 +238,7 @@ class MigrateToGitea extends CliFramework
echo " Branch protection applied\n";
} catch (\Exception $e) {
echo " Warning: post-processing issue: " . $e->getMessage() . "\n";
$this->gitea->getApiClient()->resetCircuitBreaker();
$this->mokogit->getApiClient()->resetCircuitBreaker();
}
}
@@ -248,7 +248,7 @@ class MigrateToGitea extends CliFramework
$report = "## Migration Report\n\n";
$report .= "**Date:** " . gmdate('Y-m-d H:i:s') . " UTC\n";
$report .= "**Source:** GitHub ({$org})\n";
$report .= "**Destination:** Gitea ({$giteaOrg})\n\n";
$report .= "**Destination:** MokoGit ({$mokogitOrg})\n\n";
$report .= "### Results\n\n";
$report .= "| Status | Count |\n|--------|-------|\n";
@@ -274,16 +274,16 @@ class MigrateToGitea extends CliFramework
echo $report;
// Create summary issue on Gitea
// Create summary issue on MokoGit
try {
$this->gitea->createIssue(
$giteaOrg,
$this->mokogit->createIssue(
$mokogitOrg,
'mokocli',
'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated',
'chore: GitHub → MokoGit migration report — ' . count($results['migrated']) . ' repos migrated',
$report,
['labels' => ['automation', 'type: chore']]
);
echo "Migration report issue created on Gitea.\n";
echo "Migration report issue created on MokoGit.\n";
} catch (\Exception $e) {
echo "Could not create report issue: " . $e->getMessage() . "\n";
}
@@ -296,5 +296,5 @@ class MigrateToGitea extends CliFramework
}
}
$script = new MigrateToGitea('migrate_to_gitea', 'Migrate repositories from GitHub to Gitea');
$script = new MigrateToMokoGit('migrate_to_mokogit', 'Migrate repositories from GitHub to MokoGit');
exit($script->execute());
+5 -11
View File
@@ -228,18 +228,12 @@ class PushFiles extends CliFramework
*/
private function detectRepoPlatform(string $org, string $repo): string
{
// Read platform from repo's .mokogitea/manifest.xml via API
// Read platform from the MokoGit metadata API (.mokogit/manifest.xml retired).
try {
$fileInfo = $this->adapter->getFileContents($org, $repo, '.mokogitea/manifest.xml', 'main');
$manifestData = isset($fileInfo['content']) ? base64_decode($fileInfo['content']) : '';
if (!empty($manifestData)) {
$xml = @simplexml_load_string($manifestData);
if ($xml !== false) {
$platform = (string)($xml->governance->platform ?? '');
if (!empty($platform)) {
return $platform;
}
}
$meta = $this->api->get("/repos/{$org}/{$repo}/metadata");
$platform = (string)($meta['platform'] ?? '');
if ($platform !== '') {
return $platform;
}
} catch (\Exception $e) {
// Fall through to local detection
-345
View File
@@ -1,345 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/push_manifest_xml.php
* BRIEF: Push XML manifests to all governed repositories
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
use MokoCli\ManifestParser;
class PushManifestXmlCli extends CliFramework
{
private const CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
protected function configure(): void
{
$this->setDescription('Push XML manifest.xml to all governed repositories');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
$this->addArgument('--force', 'Force overwrite even if already XML', false);
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$force = $this->getArgument('--force');
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
echo "=== mokocli XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) {
echo "Filter: {$repoFilter}\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN or GH_TOKEN environment variable required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " SKIP {$name} (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
echo " SKIP {$name} (archived)\n";
$stats['skipped']++;
continue;
}
$platform = $this->detectPlatform($repo);
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} [{$platform}] ... ";
// Generate XML manifest
$xmlContent = $parser->generate([
'name' => $name,
'org' => $giteaOrg,
'platform' => $platform,
'standards_version' => '04.07.00',
'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform),
'package_type' => ManifestParser::platformPackageType($platform),
'last_synced' => date('c'),
]);
if ($this->dryRun) {
echo "WOULD WRITE ({$platform})\n";
$stats['created']++;
continue;
}
// Clone shallow via HTTPS (token-authed)
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret, $out] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' '
. escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
fprintf(STDERR, " %s\n", $out);
$stats['failed']++;
continue;
}
// Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokocli');
if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) {
echo "SKIP (already XML)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
}
// Write manifest
@mkdir("{$workDir}/.gitea", 0755, true);
file_put_contents($manifestPath, $xmlContent);
// Delete legacy files if present
$legacyDeleted = [];
foreach (['.mokostandards', '.github/.mokostandards', '.gitea/.mokostandards', '.mokogitea/.mokostandards'] as $legacy) {
$legacyPath = "{$workDir}/{$legacy}";
if (file_exists($legacyPath)) {
unlink($legacyPath);
$legacyDeleted[] = $legacy;
}
}
// Commit
$isNew = !$existingIsXml;
$commitMsg = $isNew
? 'chore: add XML manifest.xml'
: 'chore: update manifest.xml';
if (!empty($legacyDeleted)) {
$commitMsg .= "\n\nRemoved legacy: " . implode(', ', $legacyDeleted);
}
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
foreach ($legacyDeleted as $lf) {
$this->gitCmd($workDir, 'add', $lf);
}
[$commitRet, $commitOut] = $this->gitCmd($workDir, 'commit', '-m', $commitMsg);
if ($commitRet !== 0 && str_contains($commitOut, 'nothing to commit')) {
echo "SKIP (no changes)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
if ($commitRet !== 0) {
echo "FAIL (commit)\n";
fprintf(STDERR, " %s\n", $commitOut);
$stats['failed']++;
$this->rmTree($workDir);
continue;
}
[$pushRet, $pushOut] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pushRet !== 0) {
echo "FAIL (push)\n";
fprintf(STDERR, " %s\n", $pushOut);
$stats['failed']++;
} else {
$action = $isNew ? 'CREATED' : 'UPDATED';
echo "{$action}\n";
$stats[$isNew ? 'created' : 'updated']++;
}
// Cleanup
$this->rmTree($workDir);
}
// Cleanup tmp base
@rmdir($tmpBase);
echo "\n=== Summary ===\n";
echo "Created: {$stats['created']}\n";
echo "Updated: {$stats['updated']}\n";
echo "Skipped: {$stats['skipped']}\n";
echo "Failed: {$stats['failed']}\n";
return 0;
}
private function detectPlatform(array $repo): string
{
$name = $repo['name'] ?? '';
$nameLower = strtolower($name);
$description = strtolower($repo['description'] ?? '');
$topics = $repo['topics'] ?? [];
if (in_array($name, self::CRM_PLATFORM_REPOS, true)) {
return 'crm-platform';
}
if (in_array('dolibarr-platform', $topics)) {
return 'crm-platform';
}
if (in_array('joomla-template', $topics)) {
return 'joomla-template';
}
if (in_array('joomla', $topics) || in_array('joomla-extension', $topics)) {
return 'waas-component';
}
if (in_array('dolibarr', $topics) || in_array('dolibarr-module', $topics)) {
return 'crm-module';
}
if (str_contains($nameLower, 'template') && (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'tpl'))) {
return 'joomla-template';
}
if (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'waas')) {
return 'waas-component';
}
if (str_contains($nameLower, 'doli') || str_contains($nameLower, 'crm')) {
return 'crm-module';
}
if (str_contains($description, 'joomla template')) {
return 'joomla-template';
}
if (str_contains($description, 'joomla') || str_contains($description, 'component')) {
return 'waas-component';
}
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) {
return 'crm-module';
}
if (str_contains($nameLower, 'standard')) {
return 'standards-repository';
}
return 'default-repository';
}
/**
* @return array{int, string}
*/
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open(
$command,
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
$cwd
);
if (!is_resource($proc)) {
return [1, "proc_open failed for: {$command}"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
return [$code, trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/**
* @return array{int, string}
*/
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
$this->log('ERROR', "API error (HTTP {$code}) fetching repos page {$page}");
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new PushManifestXmlCli();
exit($app->execute());
-345
View File
@@ -1,345 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/push_mokostandards_xml.php
* BRIEF: Push XML manifests to all governed repositories
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
use MokoCli\ManifestParser;
class PushMokostandardsXmlCli extends CliFramework
{
private const CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
protected function configure(): void
{
$this->setDescription('Push XML manifests to all governed repositories');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
$this->addArgument('--force', 'Force overwrite even if already XML', false);
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$force = $this->getArgument('--force');
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
echo "=== mokocli XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) {
echo "Filter: {$repoFilter}\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN or GH_TOKEN environment variable required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " SKIP {$name} (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
echo " SKIP {$name} (archived)\n";
$stats['skipped']++;
continue;
}
$platform = $this->detectPlatform($repo);
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} [{$platform}] ... ";
// Generate XML manifest
$xmlContent = $parser->generate([
'name' => $name,
'org' => $giteaOrg,
'platform' => $platform,
'standards_version' => '04.07.00',
'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform),
'package_type' => ManifestParser::platformPackageType($platform),
'last_synced' => date('c'),
]);
if ($this->dryRun) {
echo "WOULD WRITE ({$platform})\n";
$stats['created']++;
continue;
}
// Clone shallow via HTTPS (token-authed)
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret, $out] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' '
. escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
fprintf(STDERR, " %s\n", $out);
$stats['failed']++;
continue;
}
// Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokocli');
if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) {
echo "SKIP (already XML)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
}
// Write manifest
@mkdir("{$workDir}/.gitea", 0755, true);
file_put_contents($manifestPath, $xmlContent);
// Delete legacy files if present
$legacyDeleted = [];
foreach (['.mokostandards', '.github/.mokostandards'] as $legacy) {
$legacyPath = "{$workDir}/{$legacy}";
if (file_exists($legacyPath)) {
unlink($legacyPath);
$legacyDeleted[] = $legacy;
}
}
// Commit
$isNew = !$existingIsXml;
$commitMsg = $isNew
? 'chore: add XML manifest.xml'
: 'chore: update .mokostandards to XML format';
if (!empty($legacyDeleted)) {
$commitMsg .= "\n\nRemoved legacy: " . implode(', ', $legacyDeleted);
}
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
foreach ($legacyDeleted as $lf) {
$this->gitCmd($workDir, 'add', $lf);
}
[$commitRet, $commitOut] = $this->gitCmd($workDir, 'commit', '-m', $commitMsg);
if ($commitRet !== 0 && str_contains($commitOut, 'nothing to commit')) {
echo "SKIP (no changes)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
if ($commitRet !== 0) {
echo "FAIL (commit)\n";
fprintf(STDERR, " %s\n", $commitOut);
$stats['failed']++;
$this->rmTree($workDir);
continue;
}
[$pushRet, $pushOut] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pushRet !== 0) {
echo "FAIL (push)\n";
fprintf(STDERR, " %s\n", $pushOut);
$stats['failed']++;
} else {
$action = $isNew ? 'CREATED' : 'UPDATED';
echo "{$action}\n";
$stats[$isNew ? 'created' : 'updated']++;
}
// Cleanup
$this->rmTree($workDir);
}
// Cleanup tmp base
@rmdir($tmpBase);
echo "\n=== Summary ===\n";
echo "Created: {$stats['created']}\n";
echo "Updated: {$stats['updated']}\n";
echo "Skipped: {$stats['skipped']}\n";
echo "Failed: {$stats['failed']}\n";
return 0;
}
private function detectPlatform(array $repo): string
{
$name = $repo['name'] ?? '';
$nameLower = strtolower($name);
$description = strtolower($repo['description'] ?? '');
$topics = $repo['topics'] ?? [];
if (in_array($name, self::CRM_PLATFORM_REPOS, true)) {
return 'crm-platform';
}
if (in_array('dolibarr-platform', $topics)) {
return 'crm-platform';
}
if (in_array('joomla-template', $topics)) {
return 'joomla-template';
}
if (in_array('joomla', $topics) || in_array('joomla-extension', $topics)) {
return 'waas-component';
}
if (in_array('dolibarr', $topics) || in_array('dolibarr-module', $topics)) {
return 'crm-module';
}
if (str_contains($nameLower, 'template') && (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'tpl'))) {
return 'joomla-template';
}
if (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'waas')) {
return 'waas-component';
}
if (str_contains($nameLower, 'doli') || str_contains($nameLower, 'crm')) {
return 'crm-module';
}
if (str_contains($description, 'joomla template')) {
return 'joomla-template';
}
if (str_contains($description, 'joomla') || str_contains($description, 'component')) {
return 'waas-component';
}
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) {
return 'crm-module';
}
if (str_contains($nameLower, 'standard')) {
return 'standards-repository';
}
return 'default-repository';
}
/**
* @return array{int, string}
*/
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open(
$command,
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
$cwd
);
if (!is_resource($proc)) {
return [1, "proc_open failed for: {$command}"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
return [$code, trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/**
* @return array{int, string}
*/
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
$this->log('ERROR', "API error (HTTP {$code}) fetching repos page {$page}");
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new PushMokostandardsXmlCli();
exit($app->execute());
+8 -9
View File
@@ -368,7 +368,7 @@ class RepoCleanup extends CliFramework
}
// Check both workflow directories for retired workflows (supports dual-platform repos)
$wfDirs = array_unique(['.github/workflows', '.mokogitea/workflows', $this->adapter->getWorkflowDir()]);
$wfDirs = array_unique(['.github/workflows', '.mokogit/workflows', $this->adapter->getWorkflowDir()]);
foreach (self::RETIRED_WORKFLOWS as $wf) {
foreach ($wfDirs as $wfDir) {
$path = "{$wfDir}/{$wf}";
@@ -479,15 +479,14 @@ class RepoCleanup extends CliFramework
if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) {
$version = $m[1];
// Check manifest.xml for the tracked mokocli version
// Check the MokoGit metadata API for the tracked standards version
// (.mokogit/manifest.xml retired).
try {
$mokoFile = $this->api->get("/repos/{$org}/{$repo}/contents/.mokogitea/manifest.xml");
$mokoContent = base64_decode($mokoFile['content'] ?? '');
if (preg_match('/standards_version:\s*(\d{2}\.\d{2}\.\d{2})/m', $mokoContent, $vm)) {
if ($vm[1] !== self::VERSION) {
$this->logMsg(" ⚠️ Standards drift: {$vm[1]} (expected " . self::VERSION . ")");
$results['version_drift']++;
}
$meta = $this->api->get("/repos/{$org}/{$repo}/metadata");
$sv = (string)($meta['standards_version'] ?? '');
if ($sv !== '' && $sv !== self::VERSION) {
$this->logMsg(" ⚠️ Standards drift: {$sv} (expected " . self::VERSION . ")");
$results['version_drift']++;
}
} catch (\Exception $e) {
$this->api->resetCircuitBreaker();
+3 -3
View File
@@ -183,7 +183,7 @@ class UpdateDependencies extends CliFramework
// Default exclusions
$excludeRepos = array_merge($excludeRepos, [
'mokocli', '.mokogitea-private', 'org-profile',
'mokocli', '.mokogit-private', 'org-profile',
]);
try {
@@ -505,8 +505,8 @@ class UpdateDependencies extends CliFramework
}
// Commit and push
exec(sprintf('git -C %s config user.email "gitea-actions[bot]@mokoconsulting.tech"', escapeshellarg($tmpDir)));
exec(sprintf('git -C %s config user.name "gitea-actions[bot]"', escapeshellarg($tmpDir)));
exec(sprintf('git -C %s config user.email "mokogit-actions[bot]@mokoconsulting.tech"', escapeshellarg($tmpDir)));
exec(sprintf('git -C %s config user.name "mokogit-actions[bot]"', escapeshellarg($tmpDir)));
exec(sprintf('git -C %s add -A', escapeshellarg($tmpDir)));
// Check if there are actual changes
+4 -9
View File
@@ -21,14 +21,14 @@
*
* COMMANDS (run `php bin/moko list` for the full list — 97 commands)
*
* Automation sync, automation:cleanup, automation:migrate-gitea
* Automation sync, automation:cleanup, automation:migrate-mokogit
* Validation health, detect, drift, check:syntax, check:version, ...
* Release release, release:joomla, release:create, release:publish, ...
* Version version:read, version:bump, version:auto-bump, ...
* Build build:package, build:joomla, build:updates-xml, ...
* Deploy deploy:joomla, deploy:dolibarr, deploy:sftp, deploy:rollback, ...
* Repository repo:create, repo:archive, repo:rename-branch, repo:reset-dev, ...
* Bulk Operations bulk:push-workflow, bulk:push-manifest, bulk:template-joomla, ...
* Bulk Operations bulk:push-workflow, bulk:push-files, bulk:template-joomla, ...
* Maintenance maintenance:labels, maintenance:rotate-secrets, maintenance:pin-shas, ...
* Fix fix:line-endings, fix:tabs, fix:trailing, fix:permissions
* Monitoring dashboard, grafana, client:inventory, client:health-check
@@ -91,7 +91,7 @@ const COMMAND_MAP = [
'sync' => 'automation/bulk_sync.php',
'deps:update' => 'automation/update_dependencies.php',
'automation:cleanup' => 'automation/repo_cleanup.php',
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
'automation:migrate-mokogit' => 'automation/migrate_to_mokogit.php',
// Maintenance
'inventory' => 'maintenance/update_repo_inventory.php',
@@ -164,8 +164,6 @@ const COMMAND_MAP = [
// Build & package
'build:package' => 'cli/package_build.php',
'build:joomla' => 'cli/joomla_build.php',
'build:updates-xml' => 'cli/updates_xml_build.php',
'build:updates-xml-sync' => 'cli/updates_xml_sync.php',
// Platform detection & manifest
'platform:detect' => 'cli/platform_detect.php',
@@ -185,12 +183,9 @@ const COMMAND_MAP = [
// Bulk operations
'bulk:push-workflow' => 'cli/bulk_workflow_push.php',
'bulk:trigger' => 'cli/bulk_workflow_trigger.php',
'workflow:sync' => 'cli/workflow_sync.php',
'bulk:sync-rulesets' => 'cli/sync_rulesets.php',
'bulk:push-files' => 'automation/push_files.php',
'bulk:push-manifest' => 'automation/push_manifest_xml.php',
'bulk:push-mokostandards' => 'automation/push_mokostandards_xml.php',
'bulk:enrich-manifest' => 'automation/enrich_manifest_xml.php',
'bulk:enrich-mokostandards' => 'automation/enrich_mokostandards_xml.php',
'bulk:template-joomla' => 'automation/bulk_joomla_template.php',
// Deploy
+2 -2
View File
@@ -11,7 +11,7 @@
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/branch_rename.php
* VERSION: 09.38.05
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
* BRIEF: Rename a git branch via MokoGit API (create new, update PR, delete old)
*/
declare(strict_types=1);
@@ -24,7 +24,7 @@ class BranchRenameCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Rename a git branch via Gitea API (create new, update PR, delete old)');
$this->setDescription('Rename a git branch via MokoGit API (create new, update PR, delete old)');
$this->addArgument('--from', 'Source branch name', '');
$this->addArgument('--to', 'Target branch name', '');
$this->addArgument('--token', 'API token', '');
+18 -18
View File
@@ -13,7 +13,7 @@
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/bulk_workflow_push.php
* VERSION: 09.38.05
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
* BRIEF: Push a workflow file to all governed repos via the MokoGit Contents API
*/
declare(strict_types=1);
@@ -31,18 +31,18 @@ class BulkWorkflowPushCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Push a workflow file to all governed repos via the Gitea Contents API');
$this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'Gitea API token', '');
$this->setDescription('Push a workflow file to all governed repos via the MokoGit Contents API');
$this->addArgument('--mokogit-url', 'MokoGit URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--org', 'Target organization', '');
$this->addArgument('--file', 'Local workflow file to push', '');
$this->addArgument('--dest', 'Destination path in repos (default: .mokogitea/workflows/<filename>)', '');
$this->addArgument('--dest', 'Destination path in repos (default: .mokogit/workflows/<filename>)', '');
$this->addArgument('--branch', 'Target branch (default: main)', 'main');
}
protected function run(): int
{
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$token = $this->getArgument('--token');
$org = $this->getArgument('--org');
$workflowFile = $this->getArgument('--file');
@@ -70,7 +70,7 @@ class BulkWorkflowPushCli extends CliFramework
}
if ($destPath === '') {
$destPath = '.mokogitea/workflows/' . basename($workflowFile);
$destPath = '.mokogit/workflows/' . basename($workflowFile);
}
$localContent = file_get_contents($workflowFile);
@@ -82,7 +82,7 @@ class BulkWorkflowPushCli extends CliFramework
$this->log('INFO', "Pushing: {$workflowFile}");
$this->log('INFO', " -> {$destPath} (branch: {$branch})");
$this->log('INFO', " -> Org: {$org} @ {$giteaUrl}");
$this->log('INFO', " -> Org: {$org} @ {$mokogitUrl}");
if ($this->dryRun) {
$this->log('INFO', '[DRY RUN] No changes will be made.');
@@ -90,7 +90,7 @@ class BulkWorkflowPushCli extends CliFramework
echo "\n";
$repos = $this->fetchOrgRepos($giteaUrl, $token, $org);
$repos = $this->fetchOrgRepos($mokogitUrl, $token, $org);
if ($repos === null) {
return 1;
@@ -104,7 +104,7 @@ class BulkWorkflowPushCli extends CliFramework
$encodedContent = base64_encode($localContent);
foreach ($repos as $repo) {
$this->pushToRepo($giteaUrl, $token, $repo, $encodedContent, $localContent, $destPath, $branch);
$this->pushToRepo($mokogitUrl, $token, $repo, $encodedContent, $localContent, $destPath, $branch);
}
echo "\n";
@@ -115,7 +115,7 @@ class BulkWorkflowPushCli extends CliFramework
}
private function pushToRepo(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $repoFullName,
string $encodedContent,
@@ -126,7 +126,7 @@ class BulkWorkflowPushCli extends CliFramework
[$owner, $repoName] = explode('/', $repoFullName, 2);
$existing = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/repos/{$owner}/{$repoName}/contents/"
@@ -159,7 +159,7 @@ class BulkWorkflowPushCli extends CliFramework
]);
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'PUT',
"/api/v1/repos/{$owner}/{$repoName}/contents/"
@@ -189,7 +189,7 @@ class BulkWorkflowPushCli extends CliFramework
]);
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'POST',
"/api/v1/repos/{$owner}/{$repoName}/contents/"
@@ -210,7 +210,7 @@ class BulkWorkflowPushCli extends CliFramework
}
}
private function fetchOrgRepos(string $giteaUrl, string $token, string $org): ?array
private function fetchOrgRepos(string $mokogitUrl, string $token, string $org): ?array
{
$this->log('INFO', "Fetching repos from org: {$org}");
@@ -219,7 +219,7 @@ class BulkWorkflowPushCli extends CliFramework
while (true) {
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/orgs/{$org}/repos?"
@@ -261,13 +261,13 @@ class BulkWorkflowPushCli extends CliFramework
}
private function apiRequest(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $method,
string $endpoint,
?string $body = null
): array {
$url = $giteaUrl . $endpoint;
$url = $mokogitUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
+6 -6
View File
@@ -24,7 +24,7 @@ use MokoCli\CliFramework;
class BulkWorkflowTriggerCli extends CliFramework
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $mokogitUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private string $reposFile = '';
private string $org = '';
@@ -35,8 +35,8 @@ class BulkWorkflowTriggerCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Trigger a workflow across multiple repos at once');
$this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--mokogit-url', 'MokoGit URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--repos', 'File with newline-separated owner/repo list', '');
$this->addArgument('--org', 'Trigger on all repos in an org', '');
$this->addArgument('--workflow', 'Workflow file (e.g., "sync-servers.yml")', '');
@@ -46,7 +46,7 @@ class BulkWorkflowTriggerCli extends CliFramework
protected function run(): int
{
$this->giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$this->mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$this->token = $this->getArgument('--token');
$this->reposFile = $this->getArgument('--repos');
$this->org = $this->getArgument('--org');
@@ -78,7 +78,7 @@ class BulkWorkflowTriggerCli extends CliFramework
}
$this->log('INFO', "Triggering workflow \"{$this->workflow}\" on ref \"{$this->ref}\" across " . count($repos) . " repo(s).");
$this->log('INFO', "Gitea URL: {$this->giteaUrl}");
$this->log('INFO', "MokoGit URL: {$this->mokogitUrl}");
if ($this->dryRun) {
$this->log('INFO', '[DRY RUN] No requests will be sent.');
@@ -209,7 +209,7 @@ class BulkWorkflowTriggerCli extends CliFramework
private function apiRequest(string $method, string $endpoint, ?string $body = null): array
{
$url = $this->giteaUrl . $endpoint;
$url = $this->mokogitUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
+7 -7
View File
@@ -24,7 +24,7 @@ use MokoCli\CliFramework;
class ClientDashboardCli extends CliFramework
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $mokogitUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private string $org = 'MokoConsulting';
private string $outputFile = '';
@@ -36,8 +36,8 @@ class ClientDashboardCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Generate unified client dashboard HTML');
$this->addArgument('--token', 'Gitea token (or MOKOGITEA_TOKEN)', '');
$this->addArgument('--gitea-url', 'Gitea URL', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit token (or MOKOGIT_TOKEN)', '');
$this->addArgument('--mokogit-url', 'MokoGit URL', 'https://git.mokoconsulting.tech');
$this->addArgument('--org', 'Primary org (default: MokoConsulting)', 'MokoConsulting');
$this->addArgument('--output', 'Output HTML file (default: stdout)', '');
$this->addArgument('-o', 'Output HTML file (alias)', '');
@@ -48,7 +48,7 @@ class ClientDashboardCli extends CliFramework
protected function run(): int
{
$this->giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$this->mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$this->token = $this->getArgument('--token');
$this->org = $this->getArgument('--org');
$this->outputFile = $this->getArgument('--output') ?: $this->getArgument('-o');
@@ -57,11 +57,11 @@ class ClientDashboardCli extends CliFramework
$this->sslWarnDays = (int) $this->getArgument('--ssl-warn-days');
if ($this->token === '') {
$this->token = getenv('MOKOGITEA_TOKEN') ?: '';
$this->token = getenv('MOKOGIT_TOKEN') ?: '';
}
if ($this->token === '') {
$this->log('ERROR', '--token or MOKOGITEA_TOKEN required.');
$this->log('ERROR', '--token or MOKOGIT_TOKEN required.');
return 1;
}
@@ -460,7 +460,7 @@ CARD;
/** @return array{code: int, body: string} */
private function api(string $method, string $endpoint): array
{
$url = $this->giteaUrl . $endpoint;
$url = $this->mokogitUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+1 -1
View File
@@ -88,7 +88,7 @@ class ClientHealthCheckCli extends CliFramework
$checks['update_server'] = 'fail';
}
// -- Check 2: Parse updates.xml for stable version --
// -- Check 2: Parse update feed for stable version --
$stableVersion = null;
$downloadUrl = null;
+6 -6
View File
@@ -24,21 +24,21 @@ use MokoCli\CliFramework;
class ClientInventoryCli extends CliFramework
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $mokogitUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private bool $jsonOutput = false;
protected function configure(): void
{
$this->setDescription('Discover and list all client-waas repos with their server configuration status');
$this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--mokogit-url', 'MokoGit URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--json', 'Output results as JSON', false);
}
protected function run(): int
{
$this->giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$this->mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$this->token = $this->getArgument('--token');
$this->jsonOutput = (bool) $this->getArgument('--json');
@@ -47,7 +47,7 @@ class ClientInventoryCli extends CliFramework
return 1;
}
$this->log('INFO', "Scanning Gitea instance: {$this->giteaUrl}");
$this->log('INFO', "Scanning MokoGit instance: {$this->mokogitUrl}");
// Step 1: List all orgs
$orgs = $this->fetchOrgs();
@@ -241,7 +241,7 @@ class ClientInventoryCli extends CliFramework
private function apiRequest(string $method, string $endpoint, ?string $body = null): array
{
$url = $this->giteaUrl . $endpoint;
$url = $this->mokogitUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
+18 -18
View File
@@ -24,8 +24,8 @@ use MokoCli\CliFramework;
class ClientProvisionCli extends CliFramework
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $giteaToken = '';
private string $mokogitUrl = 'https://git.mokoconsulting.tech';
private string $mokogitToken = '';
private string $grafanaUrl = '';
private string $grafanaToken = '';
private string $configFile = '';
@@ -65,17 +65,17 @@ class ClientProvisionCli extends CliFramework
return 1;
}
$this->giteaToken = $this->config['gitea_token']
?? getenv('MOKOGITEA_TOKEN') ?: '';
$this->mokogitToken = $this->config['mokogit_token']
?? getenv('MOKOGIT_TOKEN') ?: '';
$this->grafanaUrl = $this->config['grafana_url']
?? getenv('GRAFANA_URL') ?: '';
$this->grafanaToken = $this->config['grafana_token']
?? getenv('GRAFANA_TOKEN') ?: '';
$this->giteaUrl = $this->config['gitea_url']
?? $this->giteaUrl;
$this->mokogitUrl = $this->config['mokogit_url']
?? $this->mokogitUrl;
if ($this->giteaToken === '') {
$this->log('ERROR', 'gitea_token or MOKOGITEA_TOKEN required.');
if ($this->mokogitToken === '') {
$this->log('ERROR', 'mokogit_token or MOKOGIT_TOKEN required.');
return 1;
}
@@ -128,7 +128,7 @@ class ClientProvisionCli extends CliFramework
{
$this->log('INFO', '[1/5] Creating repository...');
$check = $this->giteaApi(
$check = $this->mokogitApi(
'GET',
"/api/v1/repos/{$this->org}/{$this->repoName}"
);
@@ -153,7 +153,7 @@ class ClientProvisionCli extends CliFramework
'labels' => true,
]);
$resp = $this->giteaApi(
$resp = $this->mokogitApi(
'POST',
'/api/v1/repos/MokoConsulting/'
. 'Template-Client-WaaS/generate',
@@ -167,7 +167,7 @@ class ClientProvisionCli extends CliFramework
$this->log('INFO', ' OK: Repo created');
$this->giteaApi(
$this->mokogitApi(
'POST',
"/api/v1/repos/{$this->org}/{$this->repoName}/branches",
json_encode([
@@ -250,7 +250,7 @@ class ClientProvisionCli extends CliFramework
continue;
}
$resp = $this->giteaApi(
$resp = $this->mokogitApi(
'PUT',
"{$api}/{$name}",
json_encode(['data' => $value])
@@ -391,7 +391,7 @@ class ClientProvisionCli extends CliFramework
echo "\n";
$this->log('INFO', '[5/5] Provisioning summary');
echo str_repeat('=', 60) . "\n";
echo " Repo: {$this->giteaUrl}/{$this->org}/{$this->repoName}\n";
echo " Repo: {$this->mokogitUrl}/{$this->org}/{$this->repoName}\n";
echo ' Variables: ' . count($vars) . " set\n";
echo ' Secrets: ' . count($secrets) . " set\n";
echo "\n";
@@ -409,14 +409,14 @@ class ClientProvisionCli extends CliFramework
string $name,
string $value
): bool {
$resp = $this->giteaApi(
$resp = $this->mokogitApi(
'PUT',
"{$api}/{$name}",
json_encode(['value' => $value])
);
if ($resp['code'] === 404) {
$resp = $this->giteaApi(
$resp = $this->mokogitApi(
'POST',
$api,
json_encode(['name' => $name, 'value' => $value])
@@ -426,15 +426,15 @@ class ClientProvisionCli extends CliFramework
return $resp['code'] >= 200 && $resp['code'] < 300;
}
private function giteaApi(
private function mokogitApi(
string $method,
string $endpoint,
?string $body = null
): array {
return $this->httpRequest(
$this->giteaUrl . $endpoint,
$this->mokogitUrl . $endpoint,
$method,
"token {$this->giteaToken}",
"token {$this->mokogitToken}",
$body
);
}
+5 -5
View File
@@ -81,7 +81,7 @@ class CreateProjectCli extends CliFramework
}
$config = \MokoCli\Config::load();
$platformName = $config->getString('platform', 'gitea');
$platformName = $config->getString('platform', 'mokogit');
try {
$adapter = \MokoCli\PlatformAdapterFactory::create($config);
$api = $adapter->getApiClient();
@@ -89,8 +89,8 @@ class CreateProjectCli extends CliFramework
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
return 1;
}
$token = $platformName === 'gitea'
? $config->getString('gitea.token', '')
$token = $platformName === 'mokogit'
? $config->getString('mokogit.token', '')
: $config->getString('github.token', '');
$repoRoot = dirname(__DIR__, 2);
@@ -169,7 +169,7 @@ class CreateProjectCli extends CliFramework
return $failed > 0 ? 1 : 0;
}
private function graphql(string $query, array $variables, string $token, string $platformName = 'gitea'): array
private function graphql(string $query, array $variables, string $token, string $platformName = 'mokogit'): array
{
if ($platformName !== 'github') {
return [];
@@ -219,7 +219,7 @@ class CreateProjectCli extends CliFramework
private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoCli\ApiClient $apiClient = null): string
{
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
foreach (['.github/.mokostandards', '.mokogit/.mokostandards', '.mokostandards'] as $path) {
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
if (!empty($data['content'])) {
$content = base64_decode($data['content']);
+7 -23
View File
@@ -116,27 +116,11 @@ class CreateRepoCli extends CliFramework
echo " (dry-run) would set topics: " . implode(', ', $topics) . "\n";
}
echo "Step 3: Creating .mokogitea/manifest.xml...\n";
$mokoContent = "platform: {$platform}\nversion: 04.02.30\nmanaged: true\n";
if (!$this->dryRun) {
try {
$adapter->createOrUpdateFile(
$org,
$name,
'.mokogitea/manifest.xml',
$mokoContent,
'chore: add manifest.xml platform config [skip ci]'
);
echo " manifest.xml created\n";
} catch (\Exception $e) {
echo " Warning: " . $e->getMessage() . "\n";
}
} else {
echo " (dry-run) would create .mokogitea/manifest.xml\n";
}
// Repo metadata (platform, version, managed flag) is now sourced from the MokoGit
// metadata API, not a committed .mokogit/manifest.xml. Manifest creation retired.
echo "Step 4: Creating README.md...\n";
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
echo "Step 3: Creating README.md...\n";
$baseUrl = $platformName === 'mokogit' ? $config->getString('mokogit.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
$repoUrl = "{$baseUrl}/{$org}/{$name}";
$standardsUrl = "{$baseUrl}/{$org}/MokoCli";
$readmeContent = "<!--\n"
@@ -177,7 +161,7 @@ class CreateRepoCli extends CliFramework
echo " (dry-run) would create README.md\n";
}
echo "Step 5: Provisioning labels...\n";
echo "Step 4: Provisioning labels...\n";
if (!$this->dryRun) {
$labelScript = "{$repoRoot}/api/maintenance/setup_labels.php";
if (file_exists($labelScript)) {
@@ -190,7 +174,7 @@ class CreateRepoCli extends CliFramework
echo " (dry-run) would provision standard labels\n";
}
echo "Step 6: Running initial sync...\n";
echo "Step 5: Running initial sync...\n";
if (!$this->dryRun) {
$syncScript = "{$repoRoot}/api/automation/bulk_sync.php";
if (file_exists($syncScript)) {
@@ -202,7 +186,7 @@ class CreateRepoCli extends CliFramework
echo " (dry-run) would run initial sync\n";
}
echo "Step 7: Creating Project...\n";
echo "Step 6: Creating Project...\n";
if (!$this->dryRun) {
$projectScript = "{$repoRoot}/api/cli/create_project.php";
if (file_exists($projectScript)) {
+3 -15
View File
@@ -833,7 +833,7 @@ class DeployJoomla extends CliFramework
/**
* Resolve source directory with smart fallback chain:
* 1. --src-dir flag (explicit)
* 2. .mokogitea/manifest.xml <deploy><source-dir>
* 2. .mokogit/manifest.xml <deploy><source-dir>
* 3. src/ directory
* 4. htdocs/ directory
* 5. Repo root (for flat-layout extensions)
@@ -851,20 +851,8 @@ class DeployJoomla extends CliFramework
exit(1);
}
// 2. Read from .mokogitea/manifest.xml
$mokoManifest = "{$repoPath}/.mokogitea/manifest.xml";
if (file_exists($mokoManifest)) {
$xml = @simplexml_load_file($mokoManifest);
if ($xml !== false) {
$sourceDir = (string) ($xml->deploy->{'source-dir'} ?? '');
if ($sourceDir !== '') {
$dir = "{$repoPath}/{$sourceDir}";
if (is_dir($dir)) {
return $dir;
}
}
}
}
// (.mokogit/manifest.xml <deploy><source-dir> retired — rely on the resolution
// chain below.)
// 3-5. Fallback chain (source/ → src/ → htdocs/)
$resolved = SourceResolver::resolveAbsolute($repoPath);
+5 -5
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/dev_branch_reset.php
* BRIEF: Delete and recreate dev branch from main via Gitea API
* BRIEF: Delete and recreate dev branch from main via MokoGit API
*/
declare(strict_types=1);
@@ -23,9 +23,9 @@ class DevBranchResetCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Delete and recreate dev branch from main via Gitea API');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--api-base', 'Gitea API base URL', '');
$this->setDescription('Delete and recreate dev branch from main via MokoGit API');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--api-base', 'MokoGit API base URL', '');
$this->addArgument('--branch', 'Branch to reset', 'dev');
$this->addArgument('--from', 'Source branch', 'main');
$this->addArgument('--output-summary', 'Write to $GITHUB_STEP_SUMMARY', false);
@@ -33,7 +33,7 @@ class DevBranchResetCli extends CliFramework
protected function run(): int
{
$token = $this->getArgument('--token') ?: getenv('MOKOGITEA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
$token = $this->getArgument('--token') ?: getenv('MOKOGIT_TOKEN') ?: getenv('MOKOGIT_TOKEN') ?: '';
$apiBase = $this->getArgument('--api-base');
$branch = $this->getArgument('--branch');
$from = $this->getArgument('--from');
+13 -13
View File
@@ -11,7 +11,7 @@
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/joomla_metadata_validate.php
* VERSION: 09.38.05
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
* BRIEF: Validate MokoGit repo metadata against Joomla extension manifest XML
*/
declare(strict_types=1);
@@ -22,7 +22,7 @@ use MokoCli\CliFramework;
class JoomlaMetadataValidateCli extends CliFramework
{
/** Joomla element prefix map — must match MokoGitea's cleanJoomlaElement() */
/** Joomla element prefix map — must match MokoGit's cleanJoomlaElement() */
private const JOOMLA_PREFIX = [
'package' => 'pkg_',
'component' => 'com_',
@@ -34,12 +34,12 @@ class JoomlaMetadataValidateCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Validate MokoGitea repo metadata against Joomla extension manifest XML');
$this->setDescription('Validate MokoGit repo metadata against Joomla extension manifest XML');
$this->addArgument('--path', 'Repo root path (default: current directory)', '.');
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
$this->addArgument('--token', 'MokoGit API token (or MOKOGIT_TOKEN env)', '');
$this->addArgument('--org', 'MokoGit org', 'MokoConsulting');
$this->addArgument('--repo', 'Repo name (auto-detected from git if empty)', '');
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--api-base', 'MokoGit API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--ci', 'CI mode: exit 1 on any error', false);
$this->addArgument('--json', 'Output as JSON', false);
}
@@ -47,7 +47,7 @@ class JoomlaMetadataValidateCli extends CliFramework
protected function run(): int
{
$path = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
$token = $this->getArgument('--token') ?: getenv('MOKOGIT_TOKEN') ?: '';
$org = $this->getArgument('--org');
$repoName = $this->getArgument('--repo');
$apiBase = rtrim($this->getArgument('--api-base'), '/');
@@ -73,11 +73,11 @@ class JoomlaMetadataValidateCli extends CliFramework
$this->log('INFO', "Joomla manifest: {$joomlaXml['path']}");
// ── Step 2: Load MokoGitea metadata ─────────────────────────
// ── Step 2: Load MokoGit metadata ─────────────────────────
$metadata = $this->loadMetadata($path, $org, $repoName, $token, $apiBase);
if ($metadata === null) {
$this->log('ERROR', 'Could not load MokoGitea metadata');
$this->log('ERROR', 'Could not load MokoGit metadata');
return 1;
}
@@ -174,7 +174,7 @@ class JoomlaMetadataValidateCli extends CliFramework
private function loadMetadata(string $root, string $org, string $repoName, string $token, string $apiBase): ?array
{
if ($token === '') {
$this->log('ERROR', 'No API token provided (use --token or set GITEA_TOKEN env var)');
$this->log('ERROR', 'No API token provided (use --token or set MOKOGIT_TOKEN env var)');
return null;
}
@@ -202,7 +202,7 @@ class JoomlaMetadataValidateCli extends CliFramework
if ($httpCode === 404) {
$this->log('ERROR', "API endpoint not found: {$url}");
$this->log('ERROR', 'Server may need MokoGitea-Fork >= #650 (metadata endpoint rename)');
$this->log('ERROR', 'Server may need MokoGit-Fork >= #650 (metadata endpoint rename)');
return null;
}
@@ -328,7 +328,7 @@ class JoomlaMetadataValidateCli extends CliFramework
// =================================================================
/**
* Normalize extension_type map MokoGitea types to Joomla types.
* Normalize extension_type map MokoGit types to Joomla types.
*/
private function normalizeExtensionType(string $type): string
{
@@ -340,7 +340,7 @@ class JoomlaMetadataValidateCli extends CliFramework
/**
* Derive the Joomla element name from type + name.
* Replicates MokoGitea's cleanJoomlaElement() + prefix logic.
* Replicates MokoGit's cleanJoomlaElement() + prefix logic.
*/
private function deriveElement(string $type, string $name): string
{
+11 -84
View File
@@ -12,7 +12,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/joomla_release.php
* BRIEF: Joomla release pipeline build ZIP+tar.gz, upload to GitHub Release, update updates.xml
* BRIEF: Joomla release pipeline build ZIP+tar.gz, upload to MokoGit Release
*
* USAGE
* php cli/joomla_release.php --repo MokoCassiopeia --stability stable
@@ -30,7 +30,7 @@ use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactor
/**
* Joomla Release Manager
*
* Creates and manages Joomla extension releases on Gitea, including
* Creates and manages Joomla extension releases on MokoGit, including
* package building, asset upload, and update stream management.
*
* @since 04.06.00
@@ -61,7 +61,7 @@ class JoomlaRelease extends CliFramework
protected function configure(): void
{
$this->setDescription('Joomla release pipeline — build packages, upload, update updates.xml');
$this->setDescription('Joomla release pipeline — build packages, upload to MokoGit Release');
$this->addArgument('--repo', 'Repository name (e.g., MokoCassiopeia)', '');
$this->addArgument('--path', 'Local repo path (alternative to --repo)', '.');
$this->addArgument('--stability', 'Stability level: development|alpha|beta|rc|stable', 'stable');
@@ -165,20 +165,6 @@ class JoomlaRelease extends CliFramework
$this->log('INFO', "[DRY-RUN] Would upload to {$releaseTag}");
}
// ── Step 5: Update updates.xml ────────────────────────────────
$updatesXml = "{$path}/updates.xml";
$zipUrl = "https://github.com/{$repoFullName}/releases/download/{$releaseTag}/{$zipName}";
$tarUrl = "https://github.com/{$repoFullName}/releases/download/{$releaseTag}/{$tarName}";
$entry = $this->buildUpdateEntry($meta, $displayVersion, $stability, $zipUrl, $tarUrl, $sha256);
if (!$dryRun) {
$this->mergeUpdateEntry($updatesXml, $stability, $entry);
$this->log('SUCCESS', "updates.xml updated ({$stability}: {$displayVersion})");
} else {
$this->log('INFO', "[DRY-RUN] Would update updates.xml");
}
echo "\n";
$this->log('SUCCESS', "Release complete: {$displayVersion}{$releaseTag}");
@@ -425,9 +411,9 @@ class JoomlaRelease extends CliFramework
}
$releaseConfig = Config::load();
$releasePlatform = $releaseConfig->getString('platform', 'gitea');
$releaseToken = $releasePlatform === 'gitea'
? $releaseConfig->getString('gitea.token', '')
$releasePlatform = $releaseConfig->getString('platform', 'mokogit');
$releaseToken = $releasePlatform === 'mokogit'
? $releaseConfig->getString('mokogit.token', '')
: $releaseConfig->getString('github.token', '');
$acceptHeader = $releasePlatform === 'github' ? 'application/vnd.github+json' : 'application/json';
$ch = curl_init();
@@ -446,65 +432,6 @@ class JoomlaRelease extends CliFramework
curl_close($ch);
}
// ── updates.xml ──────────────────────────────────────────────────
private function buildUpdateEntry(array $meta, string $version, string $stability, string $zipUrl, string $tarUrl, string $sha256): string
{
$lines = [' <update>'];
$lines[] = " <name>{$meta['name']}</name>";
$lines[] = " <description>{$meta['name']} ({$stability})</description>";
$lines[] = " <element>{$meta['element']}</element>";
$lines[] = " <type>{$meta['type']}</type>";
$lines[] = " <version>{$version}</version>";
if ($meta['client'] !== '') {
$lines[] = " <client>{$meta['client']}</client>";
} elseif (in_array($meta['type'], ['module', 'plugin'])) {
$lines[] = ' <client>site</client>';
}
if ($meta['group'] !== '' && $meta['type'] === 'plugin') {
$lines[] = " <folder>{$meta['group']}</folder>";
}
$lines[] = ' <tags>';
$lines[] = " <tag>{$stability}</tag>";
$lines[] = ' </tags>';
$lines[] = " <infourl title=\"{$meta['name']}\">https://git.mokoconsulting.tech/" . self::ORG . "</infourl>";
$lines[] = ' <downloads>';
$lines[] = " <downloadurl type=\"full\" format=\"zip\">{$zipUrl}</downloadurl>";
$lines[] = " <downloadurl type=\"full\" format=\"tar.gz\">{$tarUrl}</downloadurl>";
$lines[] = ' </downloads>';
if ($sha256 !== '' && $sha256 !== 'dry-run') {
$lines[] = " <sha256>sha256:{$sha256}</sha256>";
}
$lines[] = " {$meta['tp']}";
if ($meta['phpMin'] !== '') {
$lines[] = " <php_minimum>{$meta['phpMin']}</php_minimum>";
}
$lines[] = ' <maintainer>Moko Consulting</maintainer>';
$lines[] = ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>';
$lines[] = ' </update>';
return implode("\n", $lines);
}
private function mergeUpdateEntry(string $xmlPath, string $stability, string $newEntry): void
{
if (!is_file($xmlPath)) {
file_put_contents($xmlPath, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<updates>\n{$newEntry}\n</updates>\n");
return;
}
$content = file_get_contents($xmlPath);
$pattern = '#\s*<update>.*?<tag>' . preg_quote($stability, '#') . '</tag>.*?</update>#s';
$content = preg_replace($pattern, '', $content);
$content = str_replace('</updates>', "{$newEntry}\n</updates>", $content);
$content = preg_replace('/\n{3,}/', "\n\n", $content);
file_put_contents($xmlPath, $content);
}
private function cloneRepo(string $repo): ?string
{
$tmpDir = sys_get_temp_dir() . "/joomla_release_{$repo}";
@@ -512,12 +439,12 @@ class JoomlaRelease extends CliFramework
$this->rmdir($tmpDir);
}
$config = Config::load();
$platform = $config->getString('platform', 'gitea');
$token = $platform === 'gitea'
? $config->getString('gitea.token', '')
$platform = $config->getString('platform', 'mokogit');
$token = $platform === 'mokogit'
? $config->getString('mokogit.token', '')
: $config->getString('github.token', '');
$cloneHost = $platform === 'gitea'
? rtrim($config->getString('gitea.url', 'https://git.mokoconsulting.tech'), '/')
$cloneHost = $platform === 'mokogit'
? rtrim($config->getString('mokogit.url', 'https://git.mokoconsulting.tech'), '/')
: 'https://github.com';
$url = "https://x-access-token:{$token}@" . preg_replace('#^https?://#', '', $cloneHost) . '/' . self::ORG . "/{$repo}.git";
$cmd = ['git', 'clone', '--depth', '1', '--quiet', $url, $tmpDir];
+5 -5
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/license_manage.php
* BRIEF: Manage license packages and keys via MokoGitea licensing API
* BRIEF: Manage license packages and keys via MokoGit licensing API
*
* Usage:
* php bin/moko license:list --org MokoConsulting
@@ -38,9 +38,9 @@ class LicenseManage extends CliFramework
protected function configure(): void
{
$this->setDescription('Manage license packages and keys via MokoGitea licensing API');
$this->setDescription('Manage license packages and keys via MokoGit licensing API');
$this->addArgument('--org', 'Organization name', '');
$this->addArgument('--api-base', 'Gitea API base URL', '');
$this->addArgument('--api-base', 'MokoGit API base URL', '');
$this->addArgument('--token', 'API token (or set GH_TOKEN env)', '');
// Package args
@@ -71,7 +71,7 @@ class LicenseManage extends CliFramework
{
// Resolve API base
$this->apiBase = $this->getArgument('--api-base')
?: getenv('GITEA_URL')
?: getenv('MOKOGIT_URL')
?: 'https://git.mokoconsulting.tech';
$this->apiBase = rtrim($this->apiBase, '/');
@@ -657,7 +657,7 @@ HELP;
if ($httpCode === 404) {
$this->log("API endpoint not found: {$path}", 'ERROR');
$this->log('The licensing API may not be deployed yet. Check MokoGitea version.', 'WARN');
$this->log('The licensing API may not be deployed yet. Check MokoGit version.', 'WARN');
return null;
}
+6 -6
View File
@@ -29,10 +29,10 @@ class ManifestDetectCli extends CliFramework
$this->addArgument('--json', 'Output as JSON', false);
$this->addArgument('--diff', 'Show diff against current manifest API values', false);
$this->addArgument('--update', 'Push detected fields to manifest API', false);
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
$this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', '');
$this->addArgument('--token', 'MokoGit API token (or MOKOGIT_TOKEN env)', '');
$this->addArgument('--api-base', 'MokoGit API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--org', 'MokoGit org', 'MokoConsulting');
$this->addArgument('--repo', 'MokoGit repo name (auto-detected from remote if empty)', '');
$this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false);
}
@@ -43,7 +43,7 @@ class ManifestDetectCli extends CliFramework
$diffMode = (bool) $this->getArgument('--diff');
$updateMode = (bool) $this->getArgument('--update');
$ghOutput = (bool) $this->getArgument('--github-output');
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
$token = $this->getArgument('--token') ?: getenv('MOKOGIT_TOKEN') ?: '';
$apiBase = rtrim($this->getArgument('--api-base'), '/');
$org = $this->getArgument('--org');
$repoName = $this->getArgument('--repo');
@@ -74,7 +74,7 @@ class ManifestDetectCli extends CliFramework
// ── Output ──────────────────────────────────────────────────
if ($diffMode || $updateMode) {
if ($token === '') {
$this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)');
$this->log('ERROR', 'API token required for --diff/--update (use --token or MOKOGIT_TOKEN env)');
return 1;
}
if ($repoName === '') {
+3 -9
View File
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
use MokoCli\{CliFramework, SourceResolver, PlatformDetector};
class ManifestElementCli extends CliFramework
{
@@ -39,14 +39,8 @@ class ManifestElementCli extends CliFramework
$repoName = $this->getArgument('--repo');
$githubOutput = (bool) $this->getArgument('--github-output');
$root = realpath($path) ?: $path;
$platform = 'generic';
$manifestXml = "{$root}/.mokogitea/manifest.xml";
if (file_exists($manifestXml)) {
$content = file_get_contents($manifestXml);
if (preg_match('/<platform>([^<]+)<\/platform>/', $content, $pm)) {
$platform = trim($pm[1]);
}
}
// .mokogit/manifest.xml retired — detect platform from repo files.
$platform = PlatformDetector::detect($root);
$extManifest = null;
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []);
foreach ($manifestFiles as $file) {
+5 -5
View File
@@ -26,10 +26,10 @@ class ManifestIntegrityCli extends CliFramework
{
$this->setDescription('Cross-check manifest fields against repo contents across the org');
$this->addArgument('--path', 'Single repo path (local mode)', '');
$this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting');
$this->addArgument('--org', 'MokoGit org (bulk mode)', 'MokoConsulting');
$this->addArgument('--repo', 'Single repo name (remote mode)', '');
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--token', 'MokoGit API token (or MOKOGIT_TOKEN env)', '');
$this->addArgument('--api-base', 'MokoGit API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--fix', 'Push fixes for detected drift', false);
$this->addArgument('--json', 'Output as JSON', false);
$this->addArgument('--quiet', 'Only show repos with issues', false);
@@ -40,14 +40,14 @@ class ManifestIntegrityCli extends CliFramework
$path = $this->getArgument('--path');
$org = $this->getArgument('--org');
$repoName = $this->getArgument('--repo');
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
$token = $this->getArgument('--token') ?: getenv('MOKOGIT_TOKEN') ?: '';
$apiBase = rtrim($this->getArgument('--api-base'), '/');
$fixMode = (bool) $this->getArgument('--fix');
$jsonMode = (bool) $this->getArgument('--json');
$quiet = (bool) $this->getArgument('--quiet');
if ($token === '') {
$this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)');
$this->log('ERROR', 'API token required (use --token or MOKOGIT_TOKEN env)');
return 1;
}
+2 -2
View File
@@ -21,7 +21,7 @@ require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
/**
* Reads the <licensing> block from .mokogitea/manifest.xml and ensures that the
* Reads the <licensing> block from .mokogit/manifest.xml and ensures that the
* Joomla extension manifest contains the correct <updateservers> and <dlid> tags.
*
* manifest.xml licensing block example:
@@ -53,7 +53,7 @@ class ManifestLicensingCli extends CliFramework
$ghOutput = (bool) $this->getArgument('--github-output');
// ── 1. Read manifest.xml ──────────────────────────────────────────
$manifestFile = "{$root}/.mokogitea/manifest.xml";
$manifestFile = "{$root}/.mokogit/manifest.xml";
if (!file_exists($manifestFile)) {
$this->log('WARN', "No manifest.xml found at {$manifestFile}");
+9 -9
View File
@@ -11,7 +11,7 @@
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_read.php
* VERSION: 09.38.05
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
* BRIEF: Read repo metadata from MokoGit manifest API, auto-detect the rest
*/
declare(strict_types=1);
@@ -27,7 +27,7 @@ class ManifestReadCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Read repo metadata from Gitea API with auto-detection fallback');
$this->setDescription('Read repo metadata from MokoGit API with auto-detection fallback');
$this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--field', 'Single field name to output', '');
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
@@ -55,7 +55,7 @@ class ManifestReadCli extends CliFramework
// ── 1. Resolve org/repo ──────────────────────────────────────────
[$org, $repo] = $this->resolveOrgRepo($root);
// ── 2. Primary: Gitea manifest API ───────────────────────────────
// ── 2. Primary: MokoGit manifest API ───────────────────────────────
$fields = null;
if ($org !== '' && $repo !== '') {
$fields = $this->fetchFromApi($org, $repo);
@@ -82,12 +82,12 @@ class ManifestReadCli extends CliFramework
return $this->outputFields($fields, $mode, $field);
}
// ── Gitea manifest API ───────────────────────────────────────────────
// ── MokoGit manifest API ───────────────────────────────────────────────
private function fetchFromApi(string $org, string $repo): ?array
{
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
$baseUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
$token = getenv('GA_TOKEN') ?: getenv('MOKOGIT_TOKEN') ?: '';
$baseUrl = getenv('MOKOGIT_URL') ?: 'https://git.mokoconsulting.tech';
$baseUrl = rtrim($baseUrl, '/');
if ($token === '') {
@@ -123,7 +123,7 @@ class ManifestReadCli extends CliFramework
return null;
}
$this->log('INFO', "Loaded metadata from Gitea manifest API ({$org}/{$repo})");
$this->log('INFO', "Loaded metadata from MokoGit manifest API ({$org}/{$repo})");
return $data;
}
@@ -370,7 +370,7 @@ class ManifestReadCli extends CliFramework
*/
private function resolveOrgRepo(string $root): array
{
// 1. GITHUB_REPOSITORY env (set in Gitea Actions / GitHub Actions)
// 1. GITHUB_REPOSITORY env (set in MokoGit Actions / GitHub Actions)
$envRepo = getenv('GITHUB_REPOSITORY') ?: '';
if ($envRepo !== '' && str_contains($envRepo, '/')) {
return explode('/', $envRepo, 2);
@@ -447,7 +447,7 @@ class ManifestReadCli extends CliFramework
break;
case 'github-output':
$outputFile = getenv('GITHUB_OUTPUT') ?: getenv('GITEA_OUTPUT') ?: '';
$outputFile = getenv('GITHUB_OUTPUT') ?: getenv('MOKOGIT_OUTPUT') ?: '';
$lines = [];
foreach ($fields as $k => $v) {
$envKey = str_replace('-', '_', $k);
+7 -7
View File
@@ -26,8 +26,8 @@ class PlatformDetectCli extends CliFramework
{
$this->setDescription('Auto-detect repository platform type and optionally update manifest');
$this->addArgument('--path', 'Local repo path to scan (default: .)', '.');
$this->addArgument('--token', 'Gitea API token for updating manifest', '');
$this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit API token for updating manifest', '');
$this->addArgument('--mokogit-url', 'MokoGit URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--owner', 'Repo owner for API update', '');
$this->addArgument('--repo', 'Repo name for API update', '');
$this->addArgument('--update', 'Update manifest.platform via API (flag)', 'false');
@@ -40,7 +40,7 @@ class PlatformDetectCli extends CliFramework
$root = realpath($path) ?: $path;
$token = $this->getArgument('--token');
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$owner = $this->getArgument('--owner');
$repo = $this->getArgument('--repo');
$doUpdate = $this->isFlagSet('--update');
@@ -79,7 +79,7 @@ class PlatformDetectCli extends CliFramework
$this->log('INFO', "Updating manifest.platform for {$owner}/{$repo} to \"{$platform}\"...");
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'PATCH',
"/api/v1/repos/{$owner}/{$repo}/metadata",
@@ -136,7 +136,7 @@ class PlatformDetectCli extends CliFramework
// 5. Platform — is mokocli itself or org-config
$repoName = basename($root);
if (in_array($repoName, ['mokocli', 'mokogitea-org-config'])) {
if (in_array($repoName, ['mokocli', 'mokogit-org-config'])) {
return 'platform';
}
@@ -152,13 +152,13 @@ class PlatformDetectCli extends CliFramework
}
private function apiRequest(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $method,
string $endpoint,
?string $body = null
): array {
$url = $giteaUrl . $endpoint;
$url = $mokogitUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
+2 -2
View File
@@ -37,8 +37,8 @@ class ReleaseCli extends CliFramework
$repoRoot = dirname(__DIR__, 2);
$syncFile = "{$repoRoot}/lib/Enterprise/RepositorySynchronizer.php";
// Check both workflow directories for the bulk-repo-sync workflow
$bulkSyncFile = file_exists("{$repoRoot}/.mokogitea/workflows/bulk-repo-sync.yml")
? "{$repoRoot}/.mokogitea/workflows/bulk-repo-sync.yml"
$bulkSyncFile = file_exists("{$repoRoot}/.mokogit/workflows/bulk-repo-sync.yml")
? "{$repoRoot}/.mokogit/workflows/bulk-repo-sync.yml"
: "{$repoRoot}/.github/workflows/bulk-repo-sync.yml";
$cleanupFile = "{$repoRoot}/templates/workflows/shared/repository-cleanup.yml.template";
+6 -6
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_body_update.php
* BRIEF: Update Gitea release body with changelog extract and checksums
* BRIEF: Update MokoGit release body with changelog extract and checksums
*/
declare(strict_types=1);
@@ -23,12 +23,12 @@ class ReleaseBodyUpdateCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Update Gitea release body with changelog extract and checksums');
$this->setDescription('Update MokoGit release body with changelog extract and checksums');
$this->addArgument('--path', 'Repo root for CHANGELOG.md', '.');
$this->addArgument('--version', 'Version string', '');
$this->addArgument('--release-tag', 'Gitea release tag', '');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--api-base', 'Gitea API base URL', '');
$this->addArgument('--release-tag', 'MokoGit release tag', '');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--api-base', 'MokoGit API base URL', '');
$this->addArgument('--zip-name', 'ZIP filename for checksum table', '');
$this->addArgument('--tar-name', 'tar.gz filename for checksum table', '');
$this->addArgument('--zip-sha', 'SHA256 of ZIP', '');
@@ -50,7 +50,7 @@ class ReleaseBodyUpdateCli extends CliFramework
$outputSummary = $this->getArgument('--output-summary');
if (empty($token)) {
$token = getenv('MOKOGITEA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
$token = getenv('MOKOGIT_TOKEN') ?: getenv('MOKOGIT_TOKEN') ?: '';
}
if (empty($version) || empty($releaseTag) || empty($token) || empty($apiBase)) {
+10 -13
View File
@@ -40,8 +40,8 @@ class ReleaseCascadeCli extends CliFramework
{
$this->setDescription('Cascade release zip to all lower stability channels');
$this->addArgument('--stability', 'Source stability channel (required)', '');
$this->addArgument('--token', 'Gitea API token (required)', '');
$this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', '');
$this->addArgument('--token', 'MokoGit API token (required)', '');
$this->addArgument('--api-base', 'MokoGit API base URL for the repo (required)', '');
}
protected function run(): int
@@ -51,10 +51,7 @@ class ReleaseCascadeCli extends CliFramework
$apiBase = rtrim($this->getArgument('--api-base'), '/');
if ($token === '') {
$envToken = getenv('MOKOGITEA_TOKEN');
if ($envToken === false || $envToken === '') {
$envToken = getenv('GITEA_TOKEN');
}
$envToken = getenv('MOKOGIT_TOKEN');
if ($envToken !== false && $envToken !== '') {
$token = $envToken;
}
@@ -85,7 +82,7 @@ class ReleaseCascadeCli extends CliFramework
}
// 1. Get source release
$sourceRelease = $this->giteaApi("{$apiBase}/releases/tags/{$sourceTag}", $token);
$sourceRelease = $this->mokogitApi("{$apiBase}/releases/tags/{$sourceTag}", $token);
if ($sourceRelease === null) {
$this->log('WARN', "No release found at tag '{$sourceTag}' — nothing to cascade.");
return 0;
@@ -179,7 +176,7 @@ class ReleaseCascadeCli extends CliFramework
}
// Find existing release at target tag
$existing = $this->giteaApi("{$apiBase}/releases/tags/{$targetTag}", $token);
$existing = $this->mokogitApi("{$apiBase}/releases/tags/{$targetTag}", $token);
if ($existing !== null && !empty($existing['id'])) {
$releaseId = (int) $existing['id'];
@@ -189,7 +186,7 @@ class ReleaseCascadeCli extends CliFramework
foreach ($existingAssets as $asset) {
$assetId = $asset['id'] ?? 0;
if ($assetId > 0) {
$this->giteaApi(
$this->mokogitApi(
"{$apiBase}/releases/{$releaseId}/assets/{$assetId}",
$token, 'DELETE'
);
@@ -201,7 +198,7 @@ class ReleaseCascadeCli extends CliFramework
'name' => $sourceVersion,
'body' => $sourceBody,
]);
$this->giteaApi(
$this->mokogitApi(
"{$apiBase}/releases/{$releaseId}",
$token, 'PATCH', $updatePayload
);
@@ -217,7 +214,7 @@ class ReleaseCascadeCli extends CliFramework
'body' => $sourceBody,
'prerelease' => ($targetTag !== 'stable'),
]);
$newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $createPayload);
$newRelease = $this->mokogitApi("{$apiBase}/releases", $token, 'POST', $createPayload);
if ($newRelease === null || empty($newRelease['id'])) {
$this->log('ERROR', " Failed to create release at tag '{$targetTag}'");
return false;
@@ -302,9 +299,9 @@ class ReleaseCascadeCli extends CliFramework
}
/**
* Make an HTTP request to the Gitea API.
* Make an HTTP request to the MokoGit API.
*/
private function giteaApi(
private function mokogitApi(
string $url,
string $token,
string $method = 'GET',
+16 -31
View File
@@ -10,25 +10,25 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_create.php
* BRIEF: Create or overwrite a Gitea release with proper naming
* BRIEF: Create or overwrite a MokoGit release with proper naming
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
use MokoCli\{CliFramework, SourceResolver, PlatformDetector};
class ReleaseCreateCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Create or overwrite a Gitea release with proper naming');
$this->setDescription('Create or overwrite a MokoGit release with proper naming');
$this->addArgument('--path', 'Repo root for manifest detection (default: .)', '.');
$this->addArgument('--version', 'Version string (required)', '');
$this->addArgument('--tag', 'Release tag name (required)', '');
$this->addArgument('--token', 'Gitea API token (required)', '');
$this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', '');
$this->addArgument('--token', 'MokoGit API token (required)', '');
$this->addArgument('--api-base', 'MokoGit API base URL for the repo (required)', '');
$this->addArgument('--branch', 'Target commitish (default: main)', 'main');
$this->addArgument('--repo', 'Repo name for fallback element detection', '');
$this->addArgument('--prerelease', 'Mark release as prerelease', false);
@@ -47,10 +47,7 @@ class ReleaseCreateCli extends CliFramework
// Allow token from environment
if ($token === '') {
$envToken = getenv('MOKOGITEA_TOKEN');
if ($envToken === false || $envToken === '') {
$envToken = getenv('GITEA_TOKEN');
}
$envToken = getenv('MOKOGIT_TOKEN');
if ($envToken !== false && $envToken !== '') {
$token = $envToken;
}
@@ -62,7 +59,7 @@ class ReleaseCreateCli extends CliFramework
$this->log('ERROR', " --branch main Target commitish (default: main)");
$this->log('ERROR', " --repo REPO Repo name for fallback element detection");
$this->log('ERROR', " --prerelease Mark release as prerelease");
$this->log('ERROR', " Token can also be set via MOKOGITEA_TOKEN or GITEA_TOKEN env var");
$this->log('ERROR', " Token can also be set via MOKOGIT_TOKEN or MOKOGIT_TOKEN env var");
return 1;
}
@@ -76,23 +73,11 @@ class ReleaseCreateCli extends CliFramework
$extName = '';
$typePrefix = '';
// Detect platform and display name from manifest.xml
$platform = 'generic';
// Detect platform from repo files (.mokogit/manifest.xml retired). Display name
// is no longer sourced from a committed manifest; falls back to the Joomla manifest
// <name> discovered below, else the repo/tag name.
$platform = PlatformDetector::detect($root);
$prettyName = '';
$manifestXml = "{$root}/.mokogitea/manifest.xml";
if (file_exists($manifestXml)) {
$content = file_get_contents($manifestXml);
if ($content !== false) {
if (preg_match('/<platform>([^<]+)<\/platform>/', $content, $pm)) {
$platform = trim($pm[1]);
}
if (preg_match('/<display-name>([^<]+)<\/display-name>/', $content, $dn)) {
$prettyName = trim($dn[1]);
} elseif (preg_match('/<name>([^<]+)<\/name>/', $content, $nm)) {
$prettyName = trim($nm[1]);
}
}
}
// Find extension manifest (Joomla XML)
$extManifest = null;
@@ -238,16 +223,16 @@ class ReleaseCreateCli extends CliFramework
// ── Delete existing release at tag (if present) ─────────────────────────────
$existing = $this->giteaApi("{$apiBase}/releases/tags/{$tag}", $token);
$existing = $this->mokogitApi("{$apiBase}/releases/tags/{$tag}", $token);
if ($existing !== null && !empty($existing['id'])) {
$existingId = $existing['id'];
echo "Deleting existing release: {$tag} (id: {$existingId})\n";
// Delete release
$this->giteaApi("{$apiBase}/releases/{$existingId}", $token, 'DELETE');
$this->mokogitApi("{$apiBase}/releases/{$existingId}", $token, 'DELETE');
// Delete tag
$this->giteaApi("{$apiBase}/tags/{$tag}", $token, 'DELETE');
$this->mokogitApi("{$apiBase}/tags/{$tag}", $token, 'DELETE');
}
// ── Create new release ──────────────────────────────────────────────────────
@@ -260,7 +245,7 @@ class ReleaseCreateCli extends CliFramework
'prerelease' => $prerelease,
]);
$newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $payload !== false ? $payload : '{}');
$newRelease = $this->mokogitApi("{$apiBase}/releases", $token, 'POST', $payload !== false ? $payload : '{}');
if ($newRelease === null || empty($newRelease['id'])) {
$this->log('ERROR', "Failed to create release at tag: {$tag}");
return 1;
@@ -274,7 +259,7 @@ class ReleaseCreateCli extends CliFramework
return 0;
}
private function giteaApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
private function mokogitApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
{
$ch = curl_init($url);
if ($ch === false) {
+16 -16
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_manage.php
* BRIEF: Create/update Gitea releases, upload assets, update release body
* BRIEF: Create/update MokoGit releases, upload assets, update release body
*/
declare(strict_types=1);
@@ -23,7 +23,7 @@ class ReleaseManageCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Create/update Gitea releases, upload assets, update release body');
$this->setDescription('Create/update MokoGit releases, upload assets, update release body');
$this->addArgument('--action', 'create | upload | update-body | delete', null);
$this->addArgument('--tag', 'Release tag name', null);
$this->addArgument('--name', 'Release name/title', null);
@@ -31,8 +31,8 @@ class ReleaseManageCli extends CliFramework
$this->addArgument('--body-file', 'Read body from file', null);
$this->addArgument('--target', 'Target branch/commitish', 'main');
$this->addArgument('--files', 'Comma-separated file paths to upload', null);
$this->addArgument('--token', 'Gitea API token', null);
$this->addArgument('--api-base', 'Gitea API base URL', null);
$this->addArgument('--token', 'MokoGit API token', null);
$this->addArgument('--api-base', 'MokoGit API base URL', null);
}
protected function run(): int
@@ -48,7 +48,7 @@ class ReleaseManageCli extends CliFramework
$apiBase = $this->getArgument('--api-base');
$files = $filesArg !== null ? array_filter(explode(',', $filesArg)) : [];
if ($token === null) {
$token = getenv('MOKOGITEA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null;
$token = getenv('MOKOGIT_TOKEN') ?: getenv('MOKOGIT_TOKEN') ?: null;
}
if ($bodyFile !== null && file_exists($bodyFile)) {
$body = file_get_contents($bodyFile);
@@ -62,12 +62,12 @@ class ReleaseManageCli extends CliFramework
$existing = $this->getReleaseByTag($apiBase, $tag, $token);
if ($existing !== null) {
$existingId = $existing['id'];
$this->releaseGiteaApi("{$apiBase}/releases/{$existingId}", 'DELETE', $token);
$this->releaseGiteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token);
$this->releaseMokoGitApi("{$apiBase}/releases/{$existingId}", 'DELETE', $token);
$this->releaseMokoGitApi("{$apiBase}/tags/{$tag}", 'DELETE', $token);
echo "Deleted previous release: {$tag} (id: {$existingId})\n";
}
$payload = json_encode(['tag_name' => $tag, 'name' => $name ?? $tag, 'body' => $body ?? '', 'target_commitish' => $target]);
$result = $this->releaseGiteaApi("{$apiBase}/releases", 'POST', $token, $payload);
$result = $this->releaseMokoGitApi("{$apiBase}/releases", 'POST', $token, $payload);
if ($result['code'] >= 200 && $result['code'] < 300) {
$releaseId = $result['data']['id'] ?? 'unknown';
echo "Release created: {$name} (tag: {$tag}, id: {$releaseId})\n";
@@ -87,7 +87,7 @@ class ReleaseManageCli extends CliFramework
return 1;
}
$releaseId = $release['id'];
$assetsResult = $this->releaseGiteaApi("{$apiBase}/releases/{$releaseId}/assets", 'GET', $token);
$assetsResult = $this->releaseMokoGitApi("{$apiBase}/releases/{$releaseId}/assets", 'GET', $token);
$existingAssets = $assetsResult['data'] ?? [];
foreach ($files as $filePath) {
$filePath = trim($filePath);
@@ -98,13 +98,13 @@ class ReleaseManageCli extends CliFramework
$fileName = basename($filePath);
foreach ($existingAssets as $asset) {
if (($asset['name'] ?? '') === $fileName) {
$this->releaseGiteaApi("{$apiBase}/releases/{$releaseId}/assets/{$asset['id']}", 'DELETE', $token);
$this->releaseMokoGitApi("{$apiBase}/releases/{$releaseId}/assets/{$asset['id']}", 'DELETE', $token);
echo "Deleted existing asset: {$fileName}\n";
break;
}
}
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . urlencode($fileName);
$result = $this->releaseGiteaApi($uploadUrl, 'POST', $token, null, $filePath);
$result = $this->releaseMokoGitApi($uploadUrl, 'POST', $token, null, $filePath);
if ($result['code'] >= 200 && $result['code'] < 300) {
echo "Uploaded: {$fileName}\n";
} else {
@@ -119,7 +119,7 @@ class ReleaseManageCli extends CliFramework
return 1;
}
$payload = json_encode(['body' => $body ?? '']);
$result = $this->releaseGiteaApi("{$apiBase}/releases/{$release['id']}", 'PATCH', $token, $payload);
$result = $this->releaseMokoGitApi("{$apiBase}/releases/{$release['id']}", 'PATCH', $token, $payload);
if ($result['code'] >= 200 && $result['code'] < 300) {
echo "Release body updated for tag: {$tag}\n";
} else {
@@ -130,8 +130,8 @@ class ReleaseManageCli extends CliFramework
case 'delete':
$existing = $this->getReleaseByTag($apiBase, $tag, $token);
if ($existing !== null) {
$this->releaseGiteaApi("{$apiBase}/releases/{$existing['id']}", 'DELETE', $token);
$this->releaseGiteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token);
$this->releaseMokoGitApi("{$apiBase}/releases/{$existing['id']}", 'DELETE', $token);
$this->releaseMokoGitApi("{$apiBase}/tags/{$tag}", 'DELETE', $token);
echo "Deleted: {$tag} (id: {$existing['id']})\n";
} else {
echo "No release found for tag: {$tag}\n";
@@ -144,7 +144,7 @@ class ReleaseManageCli extends CliFramework
return 0;
}
private function releaseGiteaApi(string $url, string $method, string $token, ?string $jsonBody = null, ?string $filePath = null): array
private function releaseMokoGitApi(string $url, string $method, string $token, ?string $jsonBody = null, ?string $filePath = null): array
{
$ch = curl_init($url);
$headers = ["Authorization: token {$token}"];
@@ -166,7 +166,7 @@ class ReleaseManageCli extends CliFramework
private function getReleaseByTag(string $apiBase, string $tag, string $token): ?array
{
$result = $this->releaseGiteaApi("{$apiBase}/releases/tags/{$tag}", 'GET', $token);
$result = $this->releaseMokoGitApi("{$apiBase}/releases/tags/{$tag}", 'GET', $token);
return ($result['code'] === 200 && isset($result['data']['id'])) ? $result['data'] : null;
}
}
+19 -19
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_mirror.php
* BRIEF: Mirror a Gitea release (with assets) to a GitHub repository
* BRIEF: Mirror a MokoGit release (with assets) to a GitHub repository
*/
declare(strict_types=1);
@@ -23,11 +23,11 @@ class ReleaseMirrorCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Mirror a Gitea release (with assets) to a GitHub repository');
$this->setDescription('Mirror a MokoGit release (with assets) to a GitHub repository');
$this->addArgument('--version', 'Version string (required)', '');
$this->addArgument('--tag', 'Release tag name (required)', '');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', '');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--api-base', 'MokoGit API base URL for the repo (required)', '');
$this->addArgument('--gh-token', 'GitHub personal access token', '');
$this->addArgument('--gh-repo', 'GitHub org/repo (required)', '');
$this->addArgument('--branch', 'Target branch (default: main)', 'main');
@@ -44,30 +44,30 @@ class ReleaseMirrorCli extends CliFramework
$branch = $this->getArgument('--branch');
// Allow tokens from environment
$token = $token ?: (getenv('MOKOGITEA_TOKEN') ?: (getenv('GITEA_TOKEN') ?: ''));
$token = $token ?: (getenv('MOKOGIT_TOKEN') ?: (getenv('MOKOGIT_TOKEN') ?: ''));
$ghToken = $ghToken ?: (getenv('GH_MIRROR_TOKEN') ?: '');
if ($version === '' || $tag === '' || $token === '' || $apiBase === '' || $ghToken === '' || $ghRepo === '') {
$this->log('ERROR', "Usage: release_mirror.php --version VER --tag TAG --token TOKEN " .
"--api-base URL --gh-token GH_MIRROR_TOKEN --gh-repo org/repo [--branch main]");
$this->log('ERROR', " --token: Gitea token (or MOKOGITEA_TOKEN / GITEA_TOKEN env)");
$this->log('ERROR', " --token: MokoGit token (or MOKOGIT_TOKEN / MOKOGIT_TOKEN env)");
$this->log('ERROR', " --gh-token: GitHub token (or GH_MIRROR_TOKEN env)");
return 1;
}
// ── Step 1: Get Gitea release by tag ─────────────────────────────────────────
// ── Step 1: Get MokoGit release by tag ─────────────────────────────────────────
echo "Fetching Gitea release: {$tag}\n";
$giteaRelease = $this->giteaApi("{$apiBase}/releases/tags/{$tag}", $token);
if (!$giteaRelease || empty($giteaRelease['id'])) {
$this->log('ERROR', "No Gitea release found with tag: {$tag}");
echo "Fetching MokoGit release: {$tag}\n";
$mokogitRelease = $this->mokogitApi("{$apiBase}/releases/tags/{$tag}", $token);
if (!$mokogitRelease || empty($mokogitRelease['id'])) {
$this->log('ERROR', "No MokoGit release found with tag: {$tag}");
return 1;
}
$giteaId = $giteaRelease['id'];
$releaseName = $giteaRelease['name'] ?? "{$version}";
$releaseBody = $giteaRelease['body'] ?? '';
$assets = $giteaRelease['assets'] ?? [];
$mokogitId = $mokogitRelease['id'];
$releaseName = $mokogitRelease['name'] ?? "{$version}";
$releaseBody = $mokogitRelease['body'] ?? '';
$assets = $mokogitRelease['assets'] ?? [];
echo " Name: {$releaseName}\n";
echo " Assets: " . count($assets) . " file(s)\n";
@@ -109,7 +109,7 @@ class ReleaseMirrorCli extends CliFramework
echo " Created GitHub release (id: {$ghReleaseId})\n";
}
// ── Step 3: Download assets from Gitea ───────────────────────────────────────
// ── Step 3: Download assets from MokoGit ───────────────────────────────────────
$tmpDir = sys_get_temp_dir() . '/moko-mirror-' . getmypid();
@mkdir($tmpDir, 0755, true);
@@ -126,7 +126,7 @@ class ReleaseMirrorCli extends CliFramework
$localPath = "{$tmpDir}/{$name}";
echo " Downloading: {$name}\n";
if (!$this->giteaDownload($downloadUrl, $token, $localPath)) {
if (!$this->mokogitDownload($downloadUrl, $token, $localPath)) {
$this->log('ERROR', " Failed to download: {$name}");
continue;
}
@@ -151,7 +151,7 @@ class ReleaseMirrorCli extends CliFramework
return 0;
}
private function giteaApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
private function mokogitApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
@@ -176,7 +176,7 @@ class ReleaseMirrorCli extends CliFramework
return json_decode($response, true) ?: null;
}
private function giteaDownload(string $url, string $token, string $dest): bool
private function mokogitDownload(string $url, string $token, string $dest): bool
{
$ch = curl_init($url);
$fp = fopen($dest, 'wb');
+18 -31
View File
@@ -10,14 +10,14 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_package.php
* BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release
* BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to MokoGit release
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
use MokoCli\{CliFramework, SourceResolver, PlatformDetector};
class ReleasePackageCli extends CliFramework
{
@@ -35,12 +35,12 @@ class ReleasePackageCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release');
$this->setDescription('Build packages (ZIP + tar.gz) with SHA-256 and upload to MokoGit release');
$this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--version', 'Release version', '');
$this->addArgument('--tag', 'Release tag name', '');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--api-base', 'Gitea API base URL', '');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--api-base', 'MokoGit API base URL', '');
$this->addArgument('--repo', 'Repo name for element detection fallback', '');
$this->addArgument('--output', 'Output directory for built packages', '');
}
@@ -61,37 +61,24 @@ class ReleasePackageCli extends CliFramework
// Allow token from environment
if ($token === '' || $token === null) {
$token = getenv('MOKOGITEA_TOKEN') ?: (getenv('GITEA_TOKEN') ?: '');
$token = getenv('MOKOGIT_TOKEN') ?: (getenv('MOKOGIT_TOKEN') ?: '');
}
if ($version === '' || $tag === '' || $token === '' || $apiBase === '') {
$this->log('ERROR', "Usage: release_package.php --path . --version VER --tag TAG --token TOKEN --api-base URL");
$this->log('ERROR', " --repo REPO Repo name for element detection fallback");
$this->log('ERROR', " --output DIR Output directory for built packages (default: sys_get_temp_dir())");
$this->log('ERROR', " Token can also be set via MOKOGITEA_TOKEN or GITEA_TOKEN env var");
$this->log('ERROR', " Token can also be set via MOKOGIT_TOKEN or MOKOGIT_TOKEN env var");
return 1;
}
$root = realpath($path) ?: $path;
// ── Read platform from .mokogitea/manifest.xml ───────────────────────
$detectedPlatform = 'generic';
// ── Detect platform from repo files (.mokogit/manifest.xml retired) ──
// entry-point is no longer sourced from a committed manifest; packaging falls back
// to its source-dir defaults (matching the previous no-manifest behavior).
$detectedPlatform = PlatformDetector::detect($root);
$detectedEntryPoint = '';
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
if (file_exists($mokoManifest)) {
$mokoXml = @simplexml_load_file($mokoManifest);
if ($mokoXml !== false) {
$rawPlatform = (string)($mokoXml->governance->platform ?? '');
if ($rawPlatform !== '') {
$detectedPlatform = match ($rawPlatform) {
'waas-component' => 'joomla',
'crm-module' => 'dolibarr',
default => $rawPlatform,
};
}
$detectedEntryPoint = (string)($mokoXml->build->{"entry-point"} ?? '');
}
}
// ── Detect element metadata from manifest XML ────────────────────────
$extElement = '';
@@ -380,7 +367,7 @@ class ReleasePackageCli extends CliFramework
}
// ── Get release ID from tag ──────────────────────────────────────────
$result = $this->giteaApiRequest("{$apiBase}/releases/tags/{$tag}", $token);
$result = $this->mokogitApiRequest("{$apiBase}/releases/tags/{$tag}", $token);
if ($result['data'] === null || !isset($result['data']['id'])) {
$this->log('ERROR', "No release found for tag: {$tag} (HTTP {$result['code']})");
return 1;
@@ -390,7 +377,7 @@ class ReleasePackageCli extends CliFramework
echo "Release ID: {$releaseId} (tag: {$tag})\n";
// ── Delete existing assets with same names ───────────────────────────
$assetsResult = $this->giteaApiRequest("{$apiBase}/releases/{$releaseId}/assets", $token);
$assetsResult = $this->mokogitApiRequest("{$apiBase}/releases/{$releaseId}/assets", $token);
$existingAssets = $assetsResult['data'] ?? [];
$uploadNames = [
@@ -407,7 +394,7 @@ class ReleasePackageCli extends CliFramework
$assetName = $asset['name'] ?? '';
$assetId = $asset['id'] ?? 0;
if (in_array($assetName, $uploadNames, true) && $assetId > 0) {
$this->giteaApiRequest("{$apiBase}/releases/{$releaseId}/assets/{$assetId}", $token, 'DELETE');
$this->mokogitApiRequest("{$apiBase}/releases/{$releaseId}/assets/{$assetId}", $token, 'DELETE');
echo "Deleted existing asset: {$assetName}\n";
}
}
@@ -428,7 +415,7 @@ class ReleasePackageCli extends CliFramework
}
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . urlencode($name);
$httpCode = $this->giteaUploadAsset($uploadUrl, $token, $localPath);
$httpCode = $this->mokogitUploadAsset($uploadUrl, $token, $localPath);
$status = ($httpCode >= 200 && $httpCode < 300) ? 'OK' : "FAILED ({$httpCode})";
echo "Upload: {$name}{$status}\n";
@@ -449,11 +436,11 @@ class ReleasePackageCli extends CliFramework
}
/**
* Perform a Gitea API request.
* Perform a MokoGit API request.
*
* @return array{data: array<string, mixed>|null, code: int}
*/
private function giteaApiRequest(string $url, string $token, string $method = 'GET', ?string $body = null): array
private function mokogitApiRequest(string $url, string $token, string $method = 'GET', ?string $body = null): array
{
$ch = curl_init($url);
if ($ch === false) {
@@ -486,7 +473,7 @@ class ReleasePackageCli extends CliFramework
/**
* Upload a file as a release asset.
*/
private function giteaUploadAsset(string $url, string $token, string $filePath): int
private function mokogitUploadAsset(string $url, string $token, string $filePath): int
{
$ch = curl_init($url);
if ($ch === false) {
+17 -17
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_promote.php
* BRIEF: Promote a Gitea release from one channel to another (rename release, tag, assets)
* BRIEF: Promote a MokoGit release from one channel to another (rename release, tag, assets)
*/
declare(strict_types=1);
@@ -23,11 +23,11 @@ class ReleasePromoteCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Promote a Gitea release from one channel to another');
$this->setDescription('Promote a MokoGit release from one channel to another');
$this->addArgument('--from', 'Source channel (or "auto")', '');
$this->addArgument('--to', 'Target channel (required)', '');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--api-base', 'Gitea API base URL for the repo', '');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--api-base', 'MokoGit API base URL for the repo', '');
$this->addArgument('--path', 'Repository root for type prefix detection', '.');
$this->addArgument('--branch', 'Target branch', 'main');
}
@@ -41,7 +41,7 @@ class ReleasePromoteCli extends CliFramework
$path = $this->getArgument('--path');
$branch = $this->getArgument('--branch');
$token = $token ?: (getenv('MOKOGITEA_TOKEN') ?: (getenv('GITEA_TOKEN') ?: null));
$token = $token ?: (getenv('MOKOGIT_TOKEN') ?: (getenv('MOKOGIT_TOKEN') ?: null));
if ($to === null || $token === null || $apiBase === null) {
$this->log('ERROR', "Usage: release_promote.php --from <channel|auto> --to <channel> --token TOKEN --api-base URL [--path .]");
@@ -64,7 +64,7 @@ class ReleasePromoteCli extends CliFramework
// ── Resolve --from auto ──────────────────────────────────────────────────────
if ($from === 'auto') {
foreach ($channelOrder as $candidate) {
$data = $this->giteaApi("{$apiBase}/releases/tags/{$candidate}", $token);
$data = $this->mokogitApi("{$apiBase}/releases/tags/{$candidate}", $token);
if ($data && !empty($data['id'])) {
$from = $candidate;
echo "Auto-detected source channel: {$from}\n";
@@ -78,7 +78,7 @@ class ReleasePromoteCli extends CliFramework
}
// ── Find source release ──────────────────────────────────────────────────────
$sourceRelease = $this->giteaApi("{$apiBase}/releases/tags/{$from}", $token);
$sourceRelease = $this->mokogitApi("{$apiBase}/releases/tags/{$from}", $token);
if (!$sourceRelease || empty($sourceRelease['id'])) {
$this->log('ERROR', "No release found with tag: {$from}");
return 1;
@@ -90,7 +90,7 @@ class ReleasePromoteCli extends CliFramework
echo "Source: {$from} (id: {$sourceId}) — {$sourceName}\n";
// ── Get source assets ────────────────────────────────────────────────────────
$assets = $this->giteaApi("{$apiBase}/releases/{$sourceId}/assets", $token) ?: [];
$assets = $this->mokogitApi("{$apiBase}/releases/{$sourceId}/assets", $token) ?: [];
echo "Assets: " . count($assets) . " file(s)\n";
// ── Download assets to temp ──────────────────────────────────────────────────
@@ -101,7 +101,7 @@ class ReleasePromoteCli extends CliFramework
$name = $asset['name'];
$downloadUrl = $asset['browser_download_url'];
echo " Downloading: {$name}\n";
$this->giteaDownload($downloadUrl, $token, "{$tmpDir}/{$name}");
$this->mokogitDownload($downloadUrl, $token, "{$tmpDir}/{$name}");
}
// ── Detect type prefix for stable promotion ──────────────────────────────────
@@ -188,15 +188,15 @@ class ReleasePromoteCli extends CliFramework
}
// ── Delete source release + tag ──────────────────────────────────────────────
$this->giteaApi("{$apiBase}/releases/{$sourceId}", $token, 'DELETE');
$this->giteaApi("{$apiBase}/tags/{$from}", $token, 'DELETE');
$this->mokogitApi("{$apiBase}/releases/{$sourceId}", $token, 'DELETE');
$this->mokogitApi("{$apiBase}/tags/{$from}", $token, 'DELETE');
echo "Deleted source: {$from} release + tag\n";
// ── Delete existing target release + tag (if any) ────────────────────────────
$existingTarget = $this->giteaApi("{$apiBase}/releases/tags/{$to}", $token);
$existingTarget = $this->mokogitApi("{$apiBase}/releases/tags/{$to}", $token);
if ($existingTarget && !empty($existingTarget['id'])) {
$this->giteaApi("{$apiBase}/releases/{$existingTarget['id']}", $token, 'DELETE');
$this->giteaApi("{$apiBase}/tags/{$to}", $token, 'DELETE');
$this->mokogitApi("{$apiBase}/releases/{$existingTarget['id']}", $token, 'DELETE');
$this->mokogitApi("{$apiBase}/tags/{$to}", $token, 'DELETE');
echo "Deleted existing target: {$to} release + tag\n";
}
@@ -217,7 +217,7 @@ class ReleasePromoteCli extends CliFramework
'prerelease' => $isPrerelease,
]);
$newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $payload);
$newRelease = $this->mokogitApi("{$apiBase}/releases", $token, 'POST', $payload);
if (!$newRelease || empty($newRelease['id'])) {
$this->log('ERROR', "Failed to create {$to} release");
return 1;
@@ -263,7 +263,7 @@ class ReleasePromoteCli extends CliFramework
return 0;
}
private function giteaApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
private function mokogitApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
@@ -288,7 +288,7 @@ class ReleasePromoteCli extends CliFramework
return json_decode($response, true) ?: null;
}
private function giteaDownload(string $url, string $token, string $dest): bool
private function mokogitDownload(string $url, string $token, string $dest): bool
{
$ch = curl_init($url);
$fp = fopen($dest, 'wb');
+13 -78
View File
@@ -27,14 +27,13 @@ class ReleasePublishCli extends CliFramework
$this->setDescription('Publish a release and update stability streams');
$this->addArgument('--path', 'Repository root (default: .)', '.');
$this->addArgument('--stability', 'Target stability: dev|alpha|beta|rc|stable (required)', '');
$this->addArgument('--token', 'Gitea API token (required)', '');
$this->addArgument('--token', 'MokoGit API token (required)', '');
$this->addArgument('--bump', 'Version bump type: patch|minor|none (default: none)', 'none');
$this->addArgument('--branch', 'Current branch (default: auto-detect)', '');
$this->addArgument('--gitea-url', 'Gitea URL', '');
$this->addArgument('--mokogit-url', 'MokoGit URL', '');
$this->addArgument('--org', 'Organization', '');
$this->addArgument('--repo', 'Repository name', '');
$this->addArgument('--repo-url', 'Repository URL for git auth', '');
$this->addArgument('--skip-update-stream', 'Skip updates.xml generation and sync (managed externally)', false);
}
protected function run(): int
@@ -44,11 +43,10 @@ class ReleasePublishCli extends CliFramework
$token = $this->getArgument('--token');
$bumpType = $this->getArgument('--bump');
$branch = $this->getArgument('--branch');
$giteaUrl = $this->getArgument('--gitea-url') ?: (getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech');
$org = $this->getArgument('--org') ?: (getenv('GITEA_ORG') ?: '');
$repo = $this->getArgument('--repo') ?: (getenv('GITEA_REPO') ?: '');
$mokogitUrl = $this->getArgument('--mokogit-url') ?: (getenv('MOKOGIT_URL') ?: 'https://git.mokoconsulting.tech');
$org = $this->getArgument('--org') ?: (getenv('MOKOGIT_ORG') ?: '');
$repo = $this->getArgument('--repo') ?: (getenv('MOKOGIT_REPO') ?: '');
$repoUrl = $this->getArgument('--repo-url');
$skipUpdateStream = $this->getArgument('--skip-update-stream');
if (empty($stability) || empty($token)) {
$this->log('ERROR', "Usage: release_publish.php --stability <dev|alpha|beta|rc|stable> --token TOKEN [options]");
@@ -57,7 +55,7 @@ class ReleasePublishCli extends CliFramework
$cli = __DIR__;
$php = '"' . PHP_BINARY . '"';
$giteaUrl = rtrim($giteaUrl, '/');
$mokogitUrl = rtrim($mokogitUrl, '/');
// Resolve path early for shell commands (Windows needs native paths)
$resolvedPath = realpath($path) ?: $path;
@@ -81,7 +79,7 @@ class ReleasePublishCli extends CliFramework
// Auto-construct repo URL for git auth if not provided
if (empty($repoUrl) && !empty($token) && !empty($org) && !empty($repo)) {
$host = preg_replace('#^https?://#', '', $giteaUrl);
$host = preg_replace('#^https?://#', '', $mokogitUrl);
$repoUrl = "https://x-access-token:{$token}@{$host}/{$org}/{$repo}.git";
}
@@ -95,7 +93,7 @@ class ReleasePublishCli extends CliFramework
));
}
$apiBase = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}";
$apiBase = "{$mokogitUrl}/api/v1/repos/{$org}/{$repo}";
// Stability ordering and suffix mapping
$allStabilities = ['dev', 'alpha', 'beta', 'rc', 'stable'];
@@ -189,11 +187,11 @@ class ReleasePublishCli extends CliFramework
$cdR = $cdPfx . escapeshellarg($root);
@shell_exec(
$cdR . " && git config --local user.email"
. " \"gitea-actions[bot]@mokoconsulting.tech\""
. " \"mokogit-actions[bot]@mokoconsulting.tech\""
);
@shell_exec(
$cdR . " && git config --local user.name"
. " \"gitea-actions[bot]\""
. " \"mokogit-actions[bot]\""
);
if (!empty($repoUrl)) {
@shell_exec(
@@ -239,8 +237,8 @@ class ReleasePublishCli extends CliFramework
@shell_exec(
$cdR . " && git commit -m "
. escapeshellarg($commitMsg)
. " --author=\"gitea-actions[bot]"
. " <gitea-actions[bot]@mokoconsulting.tech>\""
. " --author=\"mokogit-actions[bot]"
. " <mokogit-actions[bot]@mokoconsulting.tech>\""
);
$pushResult = @shell_exec(
$cdR . " && git push origin "
@@ -308,70 +306,7 @@ class ReleasePublishCli extends CliFramework
// -- Step 4: No lesser stream copies --
echo "\n--- Step 4: Skipped (no lesser stream copies) ---\n";
if ($skipUpdateStream) {
echo "\n--- Step 5: Skipped (--skip-update-stream) ---\n";
echo "\n--- Step 6: Skipped (--skip-update-stream) ---\n";
} else {
// -- Step 5: Update ONLY this stream in updates.xml --
echo "\n--- Step 5: Update {$stability} stream in updates.xml ---\n";
$streamsToWrite = [$stability];
foreach ($streamsToWrite as $stream) {
$streamVersion = $releaseVersion;
$shaFlag = !empty($sha256) ? "--sha {$sha256}" : '';
echo " Writing {$stream} stream: {$streamVersion}\n";
if (!$this->dryRun) {
passthru("{$php} {$cli}/updates_xml_build.php --path " . escapeshellarg($path)
. " --version " . escapeshellarg($streamVersion)
. " --stability " . escapeshellarg($stream)
. " --gitea-url " . escapeshellarg($giteaUrl)
. " --org " . escapeshellarg($org)
. " --repo " . escapeshellarg($repo)
. " {$shaFlag} 2>&1");
}
}
// -- Step 6: Commit updates.xml and sync to all branches --
echo "\n--- Step 6: Commit and sync updates.xml ---\n";
$root = realpath($path) ?: $path;
if (!$this->dryRun) {
$cdX = PHP_OS_FAMILY === 'Windows' ? "cd /d " : "cd ";
$cdRt = $cdX . escapeshellarg($root);
$diffCheck = trim((string) @shell_exec(
$cdRt . " && git diff --quiet updates.xml"
. " 2>&1 && echo clean || echo dirty"
));
if ($diffCheck === 'dirty') {
@shell_exec($cdRt . " && git add updates.xml");
$chMsg = "chore: update channels for"
. " {$releaseVersion} [skip ci]";
@shell_exec(
$cdRt . " && git commit -m "
. escapeshellarg($chMsg)
. " --author=\"gitea-actions[bot]"
. " <gitea-actions[bot]@mokoconsulting.tech>\""
);
@shell_exec(
$cdRt . " && git push origin "
. escapeshellarg($branch) . " 2>&1"
);
echo " Committed updates.xml\n";
}
// Sync to all branches
passthru("{$php} {$cli}/updates_xml_sync.php --path " . escapeshellarg($path)
. " --current " . escapeshellarg($branch) . " --all"
. " --version " . escapeshellarg($releaseVersion)
. " --token " . escapeshellarg($token)
. " --gitea-url " . escapeshellarg($giteaUrl)
. " --org " . escapeshellarg($org)
. " --repo " . escapeshellarg($repo) . " 2>&1");
} else {
echo "[DRY-RUN] Would commit updates.xml and sync to all branches\n";
}
}
echo "\n--- Step 5-6: Skipped (updates.xml served dynamically by MokoGit) ---\n";
echo "\n=== Release published: {$releaseVersion} ===\n";
+3 -34
View File
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
use MokoCli\{CliFramework, SourceResolver, PlatformDetector};
class ReleaseValidateCli extends CliFramework
{
@@ -49,22 +49,8 @@ class ReleaseValidateCli extends CliFramework
}
$root = realpath($path) ?: $path;
if ($platform === null) {
$manifestXml = "{$root}/.mokogitea/manifest.xml";
if (file_exists($manifestXml)) {
$mContent = file_get_contents($manifestXml);
if (preg_match('/<platform>([^<]+)<\/platform>/', $mContent, $pm)) {
$platform = trim($pm[1]);
}
}
if (in_array($platform, ['waas-component'], true)) {
$platform = 'joomla';
}
if (in_array($platform, ['crm-module'], true)) {
$platform = 'dolibarr';
}
if ($platform === null) {
$platform = 'generic';
}
// .mokogit/manifest.xml retired — detect canonical platform from repo files.
$platform = PlatformDetector::detect($root);
}
$hasSource = SourceResolver::resolveAbsolute($root) !== null;
SourceResolver::warnIfLegacy($root);
@@ -140,23 +126,6 @@ class ReleaseValidateCli extends CliFramework
$this->addVResult('Manifest version', 'FAIL', 'No <version> tag');
}
}
if (!file_exists("{$root}/updates.xml")) {
$this->addVResult('updates.xml', 'WARN', 'Not found');
} else {
$ux = file_get_contents("{$root}/updates.xml");
$uxHasVer = preg_match(
'/<version>' . preg_quote($version, '/')
. '<\/version>/',
$ux
);
$this->addVResult(
'updates.xml version',
$uxHasVer ? 'PASS' : 'FAIL',
$uxHasVer
? "`{$version}` found"
: "`{$version}` not found"
);
}
} elseif ($platform === 'dolibarr') {
$modFile = null;
foreach (SourceResolver::getCandidates() as $sd) {
+1 -25
View File
@@ -32,7 +32,6 @@ class ReleaseVerifyCli extends CliFramework
$this->addArgument('--zip-path', 'Path to ZIP file (required)', '');
$this->addArgument('--version', 'Expected version string (required)', '');
$this->addArgument('--platform', 'joomla|dolibarr|generic', 'joomla');
$this->addArgument('--updates-xml', 'Path to updates.xml for SHA256 comparison', '');
$this->addArgument('--github-output', 'Export verify_pass, verify_fail to $GITHUB_OUTPUT', false);
$this->addArgument('--output-summary', 'Write markdown table to $GITHUB_STEP_SUMMARY', false);
}
@@ -42,7 +41,6 @@ class ReleaseVerifyCli extends CliFramework
$zipPath = $this->getArgument('--zip-path');
$version = $this->getArgument('--version');
$platform = $this->getArgument('--platform');
$updatesXml = $this->getArgument('--updates-xml');
$githubOutput = $this->getArgument('--github-output');
$outputSummary = $this->getArgument('--output-summary');
@@ -95,29 +93,7 @@ class ReleaseVerifyCli extends CliFramework
}
}
// 4. SHA256 vs updates.xml
$zipSha = hash_file('sha256', $zipPath);
if ($updatesXml !== '' && file_exists($updatesXml)) {
$uxContent = file_get_contents($updatesXml);
if (preg_match('/<sha256>([^<]+)<\/sha256>/', $uxContent, $m)) {
$expectedSha = trim($m[1]);
if ($zipSha === $expectedSha) {
$this->addResult('SHA256 vs updates.xml', 'PASS', '`' . substr($zipSha, 0, 16) . '...`');
} else {
$this->addResult(
'SHA256 vs updates.xml',
'FAIL',
"ZIP=`" . substr($zipSha, 0, 16)
. "...` updates.xml=`"
. substr($expectedSha, 0, 16) . "...`"
);
}
} else {
$this->addResult('SHA256 vs updates.xml', 'WARN', 'No <sha256> in updates.xml');
}
}
// 5. Disallowed files
// 4. Disallowed files
$disallowed = ['.claude', '.mcp.json', 'TODO.md', 'todo.md', '.git', 'node_modules', '.env'];
$found = [];
$rit = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tmpDir, \RecursiveDirectoryIterator::SKIP_DOTS));
+5 -5
View File
@@ -22,7 +22,7 @@ use MokoCli\{CliFramework, Config, PlatformAdapterFactory};
* Interactive repo setup wizard.
*
* Walks through platform selection, generates config files, workflows,
* and optionally creates the repo on Gitea via API.
* and optionally creates the repo on MokoGit via API.
*
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/145
*/
@@ -54,7 +54,7 @@ class RepoWizard extends CliFramework
{
$this->setDescription('Interactive configuration wizard for new repositories');
$this->addArgument('--path', 'Directory to generate files in', '.');
$this->addArgument('--create-remote', 'Create repo on Gitea via API', false);
$this->addArgument('--create-remote', 'Create repo on MokoGit via API', false);
$this->addArgument('--non-interactive', 'Use defaults (no prompts)', false);
}
@@ -155,8 +155,8 @@ class RepoWizard extends CliFramework
break;
}
// .mokogitea/workflows
$generated[] = $this->writeFile($path, '.mokogitea/workflows/pr-check.yml',
// .mokogit/workflows
$generated[] = $this->writeFile($path, '.mokogit/workflows/pr-check.yml',
"# PR check workflow — synced from mokocli templates\n# Run: moko sync to update\n");
// .gitignore
@@ -205,7 +205,7 @@ class RepoWizard extends CliFramework
$org = $this->answers['org'];
if ($this->dryRun) {
$this->log('INFO', "[dry-run] Would create {$org}/{$this->answers['name']} on Gitea");
$this->log('INFO', "[dry-run] Would create {$org}/{$this->answers['name']} on MokoGit");
return;
}
+14 -14
View File
@@ -28,16 +28,16 @@ class ScaffoldClientCli extends CliFramework
{
$this->setDescription('Scaffold a new client-waas repo from Template-Client-WaaS');
$this->addArgument('--name', 'Client name', '');
$this->addArgument('--org', 'Gitea organization', '');
$this->addArgument('--gitea-url', 'Gitea URL', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--org', 'MokoGit organization', '');
$this->addArgument('--mokogit-url', 'MokoGit URL', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit API token', '');
}
protected function run(): int
{
$name = $this->getArgument('--name');
$org = $this->getArgument('--org');
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$token = $this->getArgument('--token');
if ($name === '' || $org === '' || $token === '') {
$this->log('ERROR', '--name, --org, and --token are required.');
@@ -45,11 +45,11 @@ class ScaffoldClientCli extends CliFramework
}
$repoName = 'client-waas-' . $name;
$this->log('INFO', "Scaffolding client repo: {$org}/{$repoName}");
$this->log('INFO', "Gitea URL: {$giteaUrl}");
$this->log('INFO', "MokoGit URL: {$mokogitUrl}");
if ($this->dryRun) {
$this->log('INFO', '[DRY RUN] Would create repo from template MokoConsulting/Template-Client-WaaS');
$this->log('INFO', "[DRY RUN] Repo: {$org}/{$repoName}");
$this->printPostSetupInstructions($repoName, $giteaUrl, $org);
$this->printPostSetupInstructions($repoName, $mokogitUrl, $org);
return 0;
}
$this->log('INFO', 'Step 1: Creating repo from template...');
@@ -65,7 +65,7 @@ class ScaffoldClientCli extends CliFramework
$response = $this->apiRequest(
'POST',
"/api/v1/repos/MokoConsulting/Template-Client-WaaS/generate",
$giteaUrl,
$mokogitUrl,
$token,
$createPayload
);
@@ -75,12 +75,12 @@ class ScaffoldClientCli extends CliFramework
}
$this->log('INFO', "Repo created: {$org}/{$repoName}");
$this->log('INFO', 'Step 2: Updating repo description...');
$this->apiRequest('PATCH', "/api/v1/repos/{$org}/{$repoName}", $giteaUrl, $token, json_encode(['description' => "{$name} WaaS site"]));
$this->apiRequest('PATCH', "/api/v1/repos/{$org}/{$repoName}", $mokogitUrl, $token, json_encode(['description' => "{$name} WaaS site"]));
$this->log('INFO', 'Step 3: Creating dev branch from main...');
$response = $this->apiRequest(
'POST',
"/api/v1/repos/{$org}/{$repoName}/branches",
$giteaUrl,
$mokogitUrl,
$token,
json_encode([
'new_branch_name' => 'dev',
@@ -92,15 +92,15 @@ class ScaffoldClientCli extends CliFramework
} else {
$this->log('WARN', "Could not create dev branch (HTTP {$response['code']}).");
}
$this->printPostSetupInstructions($repoName, $giteaUrl, $org);
$this->printPostSetupInstructions($repoName, $mokogitUrl, $org);
$this->log('INFO', 'Scaffold complete.');
return 0;
}
private function printPostSetupInstructions(string $repoName, string $giteaUrl, string $org): void
private function printPostSetupInstructions(string $repoName, string $mokogitUrl, string $org): void
{
fwrite(STDERR, "\n=== POST-SETUP INSTRUCTIONS ===\n\n"
. "Navigate to: {$giteaUrl}/{$org}/{$repoName}/settings\n\n"
. "Navigate to: {$mokogitUrl}/{$org}/{$repoName}/settings\n\n"
. "Set REPO VARIABLES:\n"
. " DEV_SYNC_HOST, DEV_SYNC_PORT, DEV_SYNC_USER, DEV_SYNC_PATH\n"
. " LIVE_SSH_HOST, LIVE_SSH_PORT, LIVE_SSH_USER, LIVE_SYNC_PATH\n\n"
@@ -109,10 +109,10 @@ class ScaffoldClientCli extends CliFramework
. "================================\n");
}
private function apiRequest(string $method, string $endpoint, string $giteaUrl, string $token, ?string $body = null): array
private function apiRequest(string $method, string $endpoint, string $mokogitUrl, string $token, ?string $body = null): array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $giteaUrl . $endpoint);
curl_setopt($ch, CURLOPT_URL, $mokogitUrl . $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Accept: application/json', "Authorization: token {$token}"]);
-456
View File
@@ -1,456 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/updates_xml_build.php
* BRIEF: Generate Joomla updates.xml from extension manifest metadata
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
class UpdatesXmlBuildCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Generate Joomla updates.xml from extension manifest metadata');
$this->addArgument('--path', 'Repository root (default: .)', '.');
$this->addArgument('--version', 'Version string (required)', '');
$this->addArgument('--stability', 'One of: stable, rc, beta, alpha, development (default: stable)', 'stable');
$this->addArgument('--sha', 'SHA-256 hash of the ZIP package', '');
$this->addArgument('--gitea-url', 'Gitea instance URL', '');
$this->addArgument('--org', 'Organization', '');
$this->addArgument('--repo', 'Repository name', '');
$this->addArgument('--output', 'Output file path (default: updates.xml in --path)', '');
$this->addArgument('--github-output', 'Export ext_element, ext_name, ext_type, ext_folder to $GITHUB_OUTPUT', false);
}
protected function run(): int
{
$path = $this->getArgument('--path');
$version = $this->getArgument('--version');
$stability = $this->getArgument('--stability');
$sha = $this->getArgument('--sha') ?: null;
$giteaUrl = $this->getArgument('--gitea-url') ?: (getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech');
$org = $this->getArgument('--org') ?: (getenv('GITEA_ORG') ?: '');
$repo = $this->getArgument('--repo') ?: (getenv('GITEA_REPO') ?: '');
$outputFile = $this->getArgument('--output') ?: null;
$githubOutput = $this->getArgument('--github-output');
if ($version === '') {
$this->log('ERROR', 'Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]');
return 1;
}
// Strip suffix — stability is applied via --stability parameter
$version = preg_replace('/-(dev|alpha|beta|rc)$/', '', $version);
$root = realpath($path) ?: $path;
// -- Read platform from .mokogitea/manifest.xml --------------------------------
$detectedPlatform = 'joomla';
$detectedName = $repo;
$detectedPackageType = '';
$detectedDisplayName = '';
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
if (file_exists($mokoManifest)) {
$mokoXml = @simplexml_load_file($mokoManifest);
if ($mokoXml !== false) {
$rawPlatform = (string)($mokoXml->governance->platform ?? '');
if ($rawPlatform !== '') {
$detectedPlatform = match ($rawPlatform) {
'waas-component' => 'joomla',
'crm-module' => 'dolibarr',
default => $rawPlatform,
};
}
$detectedName = (string)($mokoXml->identity->name ?? $repo);
$detectedDisplayName = (string)($mokoXml->identity->{"display-name"} ?? '');
$detectedPackageType = (string)($mokoXml->build->{"package-type"} ?? '');
if (empty($org)) {
$manifestOrg = (string)($mokoXml->identity->org ?? '');
if ($manifestOrg !== '') {
$org = $manifestOrg;
}
}
if (empty($repo)) {
$manifestName = (string)($mokoXml->identity->name ?? '');
if ($manifestName !== '') {
$repo = $manifestName;
}
}
}
}
// -- Fallback: detect org/repo from git remote --------------------------------
if (empty($org) || empty($repo)) {
$remoteUrl = trim(shell_exec("git -C " . escapeshellarg($root) . " remote get-url origin 2>/dev/null") ?? '');
if (preg_match('#[/:]([^/:]+)/([^/]+?)(?:\.git)?$#', $remoteUrl, $m)) {
if (empty($org)) {
$org = $m[1];
}
if (empty($repo)) {
$repo = $m[2];
}
}
}
// -- Locate Joomla manifest ---------------------------------------------------
$manifest = null;
$candidates = SourceResolver::globSource($root, 'pkg_*.xml');
foreach ($candidates as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
break;
}
}
if ($manifest === null) {
$searchDirs = ["{$root}/src", "{$root}"];
foreach ($searchDirs as $dir) {
if (!is_dir($dir)) {
continue;
}
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
break 2;
}
}
}
}
if ($manifest === null && $detectedPlatform === 'joomla') {
$this->log('ERROR', "No Joomla XML manifest found in {$root}");
return 1;
}
// -- Parse extension metadata -------------------------------------------------
$extName = '';
$extType = '';
$extElement = '';
$extClient = '';
$extFolder = '';
$targetPlatform = '';
$phpMinimum = '';
if ($manifest !== null) {
$xml = file_get_contents($manifest);
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
$extName = $m[1];
}
if (preg_match('/<extension[^>]*type="([^"]+)"/', $xml, $m)) {
$extType = $m[1];
}
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
$extElement = $m[1];
}
if (empty($extElement) && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $m)) {
$extElement = $m[1];
}
if (empty($extElement) && preg_match('/plugin="([^"]+)"/', $xml, $m)) {
$extElement = $m[1];
}
if (empty($extElement) && preg_match('/module="([^"]+)"/', $xml, $m)) {
$extElement = $m[1];
}
if (empty($extElement)) {
$fname = strtolower(pathinfo($manifest, PATHINFO_FILENAME));
if (in_array($fname, ['templatedetails', 'manifest'])) {
$extElement = strtolower(str_replace([' ', '-'], '', $repo ?: basename($root)));
} else {
$extElement = $fname;
}
}
$extElement = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $extElement);
if (preg_match('/<extension[^>]*client="([^"]+)"/', $xml, $m)) {
$extClient = $m[1];
}
if (preg_match('/<extension[^>]*group="([^"]+)"/', $xml, $m)) {
$extFolder = $m[1];
}
if (preg_match('/(<targetplatform[^\/]*\/>)/', $xml, $m)) {
$targetPlatform = $m[1];
}
if (empty($targetPlatform)) {
$targetPlatform = '<targetplatform name="joomla" version="(5|6)\..*" />';
}
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
$phpMinimum = $m[1];
}
} else {
$extName = $detectedName ?: ($repo ?: basename($root));
$extElement = strtolower(str_replace([' ', '-'], '', $extName));
$extType = $detectedPackageType ?: 'generic';
$targetPlatform = "<targetplatform name=\"{$detectedPlatform}\" version=\".*\" />";
}
if (empty($extName)) {
$extName = $repo ?: basename($root);
}
if (empty($extType)) {
$extType = 'component';
}
if (!empty($detectedDisplayName)) {
$displayName = $detectedDisplayName;
} elseif (!empty($detectedName)) {
$displayName = $detectedName;
} else {
$displayName = $extName;
}
// -- Build type prefix --------------------------------------------------------
$typePrefix = '';
switch ($extType) {
case 'plugin':
$typePrefix = "plg_{$extFolder}_";
break;
case 'module':
$typePrefix = 'mod_';
break;
case 'component':
$typePrefix = 'com_';
break;
case 'template':
$typePrefix = 'tpl_';
break;
case 'library':
$typePrefix = 'lib_';
break;
case 'package':
$typePrefix = 'pkg_';
break;
}
// -- Export to GITHUB_OUTPUT if requested -------------------------------------
if ($githubOutput) {
$ghOutput = getenv('GITHUB_OUTPUT');
$lines = [
"ext_element={$extElement}",
"ext_name={$extName}",
"ext_type={$extType}",
"ext_folder={$extFolder}",
"type_prefix={$typePrefix}",
];
if ($ghOutput) {
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
$this->log('INFO', "Exported " . count($lines) . " fields to GITHUB_OUTPUT");
} else {
foreach ($lines as $line) {
echo "{$line}\n";
}
}
}
// -- Stability suffix map -----------------------------------------------------
$stabilitySuffixMap = [
'stable' => '',
'rc' => '-rc',
'beta' => '-beta',
'alpha' => '-alpha',
'development' => '-dev',
'dev' => '-dev',
];
$stabilityTagMap = [
'stable' => 'stable',
'rc' => 'rc',
'beta' => 'beta',
'alpha' => 'alpha',
'development' => 'dev',
'dev' => 'dev',
];
$releaseTagMap = [
'stable' => 'stable',
'rc' => 'release-candidate',
'beta' => 'beta',
'alpha' => 'alpha',
'development' => 'development',
'dev' => 'development',
];
$primarySuffix = $stabilitySuffixMap[$stability] ?? '';
$primaryVersion = $version . $primarySuffix;
$clientTag = '';
if (!empty($extClient)) {
$clientTag = " <client>{$extClient}</client>";
} else {
$clientTag = ' <client>site</client>';
}
$folderTag = '';
if (!empty($extFolder) && $extType === 'plugin') {
$folderTag = " <folder>{$extFolder}</folder>";
}
$phpTag = '';
if (!empty($phpMinimum)) {
$phpTag = " <php_minimum>{$phpMinimum}</php_minimum>";
}
$shaTag = '';
if (!empty($sha)) {
$shaTag = " <sha256>{$sha}</sha256>";
}
// -- Write ONLY the single channel being released --------------------------------
$entries = [];
$giteaTag = $releaseTagMap[$stability] ?? $stability;
$channelVersion = $version . ($stabilitySuffixMap[$stability] ?? '');
$channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$giteaTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip";
$channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$giteaTag}";
$joomlaTag = $stabilityTagMap[$stability] ?? $stability;
$changelogUrl = "{$giteaUrl}/{$org}/{$repo}/raw/branch/main/CHANGELOG.md";
$entries[] = $this->buildEntry(
$joomlaTag,
$channelVersion,
$channelDownloadUrl,
$displayName,
$stability,
$extElement,
$extType,
$clientTag,
$folderTag,
$channelInfoUrl,
$targetPlatform,
$phpTag,
$shaTag,
$changelogUrl
);
// -- Preserve existing entries for channels not being updated -----------------
$dest = $outputFile ?? "{$root}/updates.xml";
$preservedEntries = [];
if (file_exists($dest)) {
$existingXml = @simplexml_load_file($dest);
if ($existingXml) {
$writtenTag = $joomlaTag;
$writtenAliases = [$writtenTag];
if ($writtenTag === 'dev') {
$writtenAliases[] = 'development';
}
if ($writtenTag === 'development') {
$writtenAliases[] = 'dev';
}
foreach ($existingXml->update as $existingUpdate) {
$existingTag = '';
if (isset($existingUpdate->tags->tag)) {
$existingTag = (string) $existingUpdate->tags->tag;
}
if (!empty($existingTag) && !in_array($existingTag, $writtenAliases, true)) {
$preservedEntries[] = ' ' . trim($existingUpdate->asXML());
}
}
}
}
// -- Write updates.xml --------------------------------------------------------
$year = date('Y');
$output = <<<XML
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) {$year} Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: {$primaryVersion}
-->
<updates>
XML;
$allEntries = array_merge($preservedEntries, $entries);
$stabilityOrder = ['dev' => 0, 'development' => 0, 'alpha' => 1, 'beta' => 2, 'rc' => 3, 'stable' => 4];
usort($allEntries, function ($a, $b) use ($stabilityOrder) {
preg_match('/<tag>([^<]+)<\/tag>/', $a, $ma);
preg_match('/<tag>([^<]+)<\/tag>/', $b, $mb);
return ($stabilityOrder[$ma[1] ?? ''] ?? 99) - ($stabilityOrder[$mb[1] ?? ''] ?? 99);
});
$output .= "\n" . implode("\n", $allEntries) . "\n</updates>\n";
$dest = $outputFile ?? "{$root}/updates.xml";
file_put_contents($dest, $output);
$channelCount = count($entries);
echo "updates.xml: {$primaryVersion} ({$channelCount} channel(s), stability={$stability})\n";
echo "Output: {$dest}\n";
return 0;
}
private function buildEntry(
string $tagName,
string $entryVersion,
string $entryDownloadUrl,
string $displayName,
string $stabilityLabel,
string $extElement,
string $extType,
string $clientTag,
string $folderTag,
string $infoUrl,
string $targetPlatform,
string $phpTag,
string $shaTag,
string $changelogUrl = ''
): string {
$lines = [];
$lines[] = ' <update>';
$lines[] = " <name>{$displayName}</name>";
$lines[] = " <description>{$displayName} {$stabilityLabel} build.</description>";
$prefixMap = [
'package' => 'pkg_',
'module' => 'mod_',
'component' => 'com_',
'library' => 'lib_',
];
$dbElement = isset($prefixMap[$extType]) ? $prefixMap[$extType] . $extElement : $extElement;
$lines[] = " <element>{$dbElement}</element>";
$lines[] = " <type>{$extType}</type>";
$lines[] = $clientTag;
$lines[] = " <version>{$entryVersion}</version>";
$lines[] = " <creationDate>" . date('Y-m-d') . "</creationDate>";
if (!empty($folderTag)) {
$lines[] = $folderTag;
}
$lines[] = " <infourl title='{$displayName}'>{$infoUrl}</infourl>";
$lines[] = ' <downloads>';
$lines[] = " <downloadurl type='full' format='zip'>{$entryDownloadUrl}</downloadurl>";
$lines[] = ' </downloads>';
if (!empty($shaTag)) {
$lines[] = $shaTag;
}
$lines[] = " <tags><tag>{$tagName}</tag></tags>";
if (!empty($changelogUrl)) {
$lines[] = " <changelogurl>{$changelogUrl}</changelogurl>";
}
$lines[] = ' <maintainer>Moko Consulting</maintainer>';
$lines[] = ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>';
$lines[] = " {$targetPlatform}";
if (!empty($phpTag)) {
$lines[] = $phpTag;
}
$lines[] = ' </update>';
return implode("\n", $lines);
}
}
$app = new UpdatesXmlBuildCli();
exit($app->execute());
-228
View File
@@ -1,228 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/updates_xml_sync.php
* VERSION: 09.38.05
* BRIEF: Sync updates.xml to target branches via Gitea API
* NOTE: Called by pre-release and auto-release workflows after updates.xml
* is modified on the current branch. Pushes the file to other branches
* without requiring a git checkout (avoids merge conflicts).
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
class UpdatesXmlSyncCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Sync updates.xml to target branches via Gitea API');
$this->addArgument('--path', 'Repository root containing updates.xml', '.');
$this->addArgument('--branches', 'Comma-separated target branches to sync to', 'main,dev');
$this->addArgument('--all', 'Auto-discover all branches via Gitea API', false);
$this->addArgument('--current', 'Current branch to skip (required)', '');
$this->addArgument('--version', 'Version string for commit message', '');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--gitea-url', 'Gitea instance URL', '');
$this->addArgument('--org', 'Organization', '');
$this->addArgument('--repo', 'Repository name', '');
}
protected function run(): int
{
$path = $this->getArgument('--path');
$branches = $this->getArgument('--branches');
$discoverAll = $this->getArgument('--all');
$current = $this->getArgument('--current');
$version = $this->getArgument('--version');
$token = $this->getArgument('--token');
$giteaUrl = $this->getArgument('--gitea-url');
$org = $this->getArgument('--org');
$repo = $this->getArgument('--repo');
// Fall back to environment variables
if ($token === '') {
$token = getenv('MOKOGITEA_TOKEN') ?: '';
}
if ($giteaUrl === '') {
$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
}
if ($org === '') {
$org = getenv('GITEA_ORG') ?: '';
}
if ($repo === '') {
$repo = getenv('GITEA_REPO') ?: '';
}
if ($current === '') {
$this->log('ERROR', '--current is required');
return 1;
}
if ($token === '') {
$this->log('ERROR', '--token or MOKOGITEA_TOKEN env is required');
return 1;
}
if ($org === '' || $repo === '') {
$this->log('ERROR', '--org and --repo (or GITEA_ORG/GITEA_REPO env) are required');
return 1;
}
// Auto-discover branches if --all flag is set
if ($discoverAll) {
$apiUrl = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}/branches?limit=50";
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}", 'Accept: application/json'],
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($ch);
curl_close($ch);
$branchList = json_decode($response ?: '[]', true) ?: [];
$discovered = [];
foreach ($branchList as $b) {
$name = $b['name'] ?? '';
if (
$name !== '' && $name !== $current
&& !str_starts_with($name, 'version/')
&& !str_starts_with($name, 'feature/')
&& !str_starts_with($name, 'patch/')
) {
$discovered[] = $name;
}
}
if (!empty($discovered)) {
$branches = implode(',', $discovered);
echo "Discovered branches: {$branches}\n";
}
}
$updatesFile = rtrim($path, '/') . '/updates.xml';
if (!file_exists($updatesFile)) {
$this->log('ERROR', "No updates.xml found at {$updatesFile}");
return 0;
}
$content = file_get_contents($updatesFile);
$encoded = base64_encode($content);
$giteaUrl = rtrim($giteaUrl, '/');
$apiBase = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}";
$vLabel = $version !== '' ? " {$version}" : '';
$targets = array_filter(
array_map('trim', explode(',', $branches)),
fn($b) => $b !== '' && $b !== $current
);
if (empty($targets)) {
$this->log('ERROR', "No target branches to sync to (current: {$current})");
return 0;
}
$synced = 0;
$failed = 0;
foreach ($targets as $branch) {
$this->log('INFO', "Syncing updates.xml -> {$branch}...");
$sha = $this->getFileSha($apiBase, $token, $branch);
if ($sha === null) {
$this->warning("could not get SHA from {$branch}");
$failed++;
continue;
}
$ok = $this->putFile(
$apiBase,
$token,
$branch,
$encoded,
$sha,
"chore: sync updates.xml{$vLabel} from {$current} [skip ci]"
);
if ($ok) {
$this->log('INFO', "Synced to {$branch}");
$synced++;
} else {
$this->warning("push to {$branch} failed");
$failed++;
}
}
$this->log('INFO', "Done: {$synced} synced, {$failed} failed");
return $failed > 0 ? 1 : 0;
}
private function getFileSha(string $apiBase, string $token, string $branch): ?string
{
$resp = $this->apiCall('GET', "{$apiBase}/contents/updates.xml?ref={$branch}", $token);
return $resp['sha'] ?? null;
}
private function putFile(
string $apiBase,
string $token,
string $branch,
string $encoded,
string $sha,
string $msg
): bool {
$resp = $this->apiCall('PUT', "{$apiBase}/contents/updates.xml", $token, [
'content' => $encoded,
'sha' => $sha,
'message' => $msg,
'branch' => $branch,
]);
return $resp !== null;
}
private function apiCall(string $method, string $url, string $token, ?array $data = null): ?array
{
$headers = [
"Authorization: token {$token}",
'Content-Type: application/json',
'Accept: application/json',
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
if ($data !== null) {
curl_setopt(
$ch,
CURLOPT_POSTFIELDS,
json_encode($data, JSON_UNESCAPED_SLASHES)
);
}
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($code >= 200 && $code < 300)
? (json_decode($body, true) ?: [])
: null;
}
}
$app = new UpdatesXmlSyncCli();
exit($app->execute());
+103 -14
View File
@@ -68,16 +68,9 @@ class VersionAutoBumpCli extends CliFramework
$cli = __DIR__;
$php = '"' . PHP_BINARY . '"';
// Auto-detect watch path from manifest.xml if not provided
if (empty($watchPath)) {
$manifestFile = realpath($path) . '/.mokogitea/manifest.xml';
if (file_exists($manifestFile)) {
$xml = @simplexml_load_file($manifestFile);
if ($xml && isset($xml->build->{'entry-point'})) {
$watchPath = (string) $xml->build->{'entry-point'};
}
}
}
// Watch-path was previously read from .mokogit/manifest.xml <build><entry-point>.
// That manifest is retired; when --watch-path is not supplied we simply skip the
// change-gate and always bump (the prior no-manifest behavior).
// Check if code files actually changed (skip bump for docs/config-only changes)
$shouldBump = true;
@@ -142,6 +135,14 @@ class VersionAutoBumpCli extends CliFramework
// Step 4: Version consistency check and fix
exec("{$php} {$cli}/version_check.php --path " . escapeshellarg($path) . " --fix 2>&1", $checkOutput);
// Step 4b (#351/#354): keep Joomla #__schemas in lockstep with the stamped manifest.
// Must run AFTER version_set_platform/version_check have stamped each manifest <version>,
// and BEFORE the git commit below so markers are staged in the same bump commit.
$markerRoot = realpath($path) ?: $path;
foreach ($this->writeSchemaMarkers($markerRoot) as $marker) {
echo "Schema marker: {$marker}\n";
}
// Re-read version (now includes suffix from version_set_platform)
$suffixMap = [
'dev' => '-dev',
@@ -176,11 +177,11 @@ class VersionAutoBumpCli extends CliFramework
$cdRoot = $cd . escapeshellarg($root);
@shell_exec(
$cdRoot . " && git config --local user.email"
. " \"gitea-actions[bot]@mokoconsulting.tech\""
. " \"mokogit-actions[bot]@mokoconsulting.tech\""
);
@shell_exec(
$cdRoot . " && git config --local user.name"
. " \"gitea-actions[bot]\""
. " \"mokogit-actions[bot]\""
);
if (!empty($repoUrl)) {
@@ -196,8 +197,8 @@ class VersionAutoBumpCli extends CliFramework
: "chore(version): set {$stability} suffix {$displayVersion} [skip ci]";
@shell_exec(
$cdRoot . " && git commit -m " . escapeshellarg($commitMsg)
. " --author=\"gitea-actions[bot]"
. " <gitea-actions[bot]@mokoconsulting.tech>\""
. " --author=\"mokogit-actions[bot]"
. " <mokogit-actions[bot]@mokoconsulting.tech>\""
);
$pushResult = @shell_exec(
@@ -209,6 +210,94 @@ class VersionAutoBumpCli extends CliFramework
echo "Bumped to {$displayVersion}\n";
return 0;
}
/**
* Issues #351/#354 — keep Joomla #__schemas in lockstep with the manifest version.
*
* Joomla advances #__schemas only to the highest schema-update file whose version is
* <= the manifest <version>. When auto-bump climbs the manifest without adding a matching
* marker, #__schemas freezes at the last real migration and com_installer perpetually
* reports the component "a schema version behind". For every Joomla extension manifest that
* declares <update><schemas><schemapath ...>, write a comment-only no-op marker named
* exactly <manifest-version>.sql (idempotent). Reading the version back out of the stamped
* manifest guarantees the filename equals never exceeds the manifest version.
*
* @param string $root Repository root.
* @return string[] Marker paths created (each suffixed " (would create)" under --dry-run).
*/
private function writeSchemaMarkers(string $root): array
{
$created = [];
// Joomla manifests may sit at the repo root or under a source/packages tree.
$manifests = array_unique(array_merge(
glob("{$root}/*.xml") ?: [],
glob("{$root}/*/*.xml") ?: [],
glob("{$root}/*/packages/*/*.xml") ?: []
));
foreach ($manifests as $file) {
$xml = @simplexml_load_file($file);
if ($xml === false) {
continue;
}
// Only Joomla extension manifests that actually declare a schema update path.
if (!isset($xml->update->schemas->schemapath)) {
continue;
}
$version = trim((string) $xml->version);
// Guard: only stamp when the manifest carries a well-formed platform version.
if (!preg_match('/^\d{2}\.\d{2}\.\d{2}((?:-(?:dev|alpha|beta|rc))+)?$/', $version)) {
continue;
}
// Resolve the admin-relative root: <administration><files folder="admin">.
$baseDir = dirname($file);
$adminFolder = isset($xml->administration->files['folder'])
? trim((string) $xml->administration->files['folder'])
: '';
$adminDir = $adminFolder !== '' ? "{$baseDir}/{$adminFolder}" : $baseDir;
// One marker per driver-specific schemapath (mysql, postgresql, …).
foreach ($xml->update->schemas->schemapath as $schemapath) {
$rel = trim((string) $schemapath);
if ($rel === '') {
continue;
}
$dir = "{$adminDir}/{$rel}";
$marker = "{$dir}/{$version}.sql";
// Idempotent: never overwrite an existing marker (real or prior no-op).
if (file_exists($marker)) {
continue;
}
if ($this->dryRun) {
$created[] = "{$marker} (would create)";
continue;
}
if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
$this->log('WARNING', "Could not create schemapath dir: {$dir}");
continue;
}
$body = "/* {$version} — no schema changes (auto version marker) */\n";
if (file_put_contents($marker, $body) !== false) {
$created[] = $marker;
}
}
}
return $created;
}
}
$app = new VersionAutoBumpCli();
+4 -34
View File
@@ -44,24 +44,8 @@ class VersionBumpCli extends CliFramework
$mokoVersion = null;
$existingSuffix = '';
$versionPrefix = '';
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
$mokoContent = '';
if (file_exists($mokoManifest)) {
$mokoContent = file_get_contents($mokoManifest);
if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?</version>#', $mokoContent, $m)) {
$mokoVersion = $m[1];
$existingSuffix = $m[2] ?? '';
}
// Read version_prefix from manifest.xml (supports nested and flat structure)
$xml = @simplexml_load_file($mokoManifest);
if ($xml !== false) {
$prefix = (string)($xml->identity->version_prefix ?? '');
if ($prefix === '') {
$prefix = (string)($xml->version_prefix ?? '');
}
$versionPrefix = $prefix;
}
}
// .mokogit/manifest.xml retired — no longer a version/prefix source ($versionPrefix
// stays empty; README.md and platform files below drive the bump).
$readmeVersion = null;
$readme = "{$root}/README.md";
$readmeContent = '';
@@ -172,19 +156,8 @@ class VersionBumpCli extends CliFramework
}
$newBase = sprintf('%02d.%02d.%02d', $major, $minor, $patch);
$newFull = $newBase . $existingSuffix;
if (file_exists($mokoManifest) && !empty($mokoContent)) {
$pattern = '#<version>\d{2}\.\d{2}\.\d{2}'
. '(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#';
$updated = preg_replace(
$pattern,
"<version>{$newFull}</version>",
$mokoContent,
1
);
if ($updated !== null) {
file_put_contents($mokoManifest, $updated);
}
}
// .mokogit/manifest.xml retired — nothing to write back there; README.md and
// platform manifests below carry the bumped version.
if (file_exists($readme) && !empty($readmeContent)) {
if (!empty($versionPrefix)) {
// Prefix-aware README replacement: preserve prefix, replace only version part
@@ -342,9 +315,6 @@ class VersionBumpCli extends CliFramework
if (in_array($relPath, $updatedFiles ?? [], true)) {
continue;
}
if (strpos($relPath, '.mokogitea/manifest.xml') !== false) {
continue;
}
$content = @file_get_contents($filePath);
if ($content === false) {
continue;
+13 -13
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/version_bump_remote.php
* BRIEF: Bump version in manifest XML and CHANGELOG.md on a remote branch via Gitea API
* BRIEF: Bump version in manifest XML and CHANGELOG.md on a remote branch via MokoGit API
*/
declare(strict_types=1);
@@ -23,15 +23,15 @@ class VersionBumpRemoteCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Bump version in manifest XML and CHANGELOG.md on a remote branch via Gitea API');
$this->setDescription('Bump version in manifest XML and CHANGELOG.md on a remote branch via MokoGit API');
$this->addArgument('--path', 'Repository root', '.');
$this->addArgument('--branch', 'Target branch to bump (required)', null);
$this->addArgument('--bump', 'Bump type: patch | minor | major', 'minor');
$this->addArgument('--token', 'Gitea API token (or MOKOGITEA_TOKEN env var)', null);
$this->addArgument('--api-base', 'Gitea API base URL for the repo', null);
$this->addArgument('--token', 'MokoGit API token (or MOKOGIT_TOKEN env var)', null);
$this->addArgument('--api-base', 'MokoGit API base URL for the repo', null);
$this->addArgument('--no-changelog', 'Skip CHANGELOG.md bump', false);
$this->addArgument('--repo', 'Repository path (owner/repo)', null);
$this->addArgument('--gitea-url', 'Gitea instance URL', null);
$this->addArgument('--mokogit-url', 'MokoGit instance URL', null);
}
protected function run(): int
@@ -43,15 +43,15 @@ class VersionBumpRemoteCli extends CliFramework
$apiBase = $this->getArgument('--api-base');
$noChangelog = (bool) $this->getArgument('--no-changelog');
$repo = $this->getArgument('--repo');
$giteaUrl = $this->getArgument('--gitea-url');
$mokogitUrl = $this->getArgument('--mokogit-url');
if ($token === null) {
$token = getenv('MOKOGITEA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null;
$token = getenv('MOKOGIT_TOKEN') ?: getenv('MOKOGIT_TOKEN') ?: null;
}
if ($giteaUrl === null) {
$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
if ($mokogitUrl === null) {
$mokogitUrl = getenv('MOKOGIT_URL') ?: 'https://git.mokoconsulting.tech';
}
if ($apiBase === null && $repo !== null) {
$apiBase = rtrim($giteaUrl, '/') . '/api/v1/repos/' . $repo;
$apiBase = rtrim($mokogitUrl, '/') . '/api/v1/repos/' . $repo;
}
if ($branch === null || $token === null || $apiBase === null) {
$this->log('ERROR', "Usage: version_bump_remote.php --branch BRANCH --token TOKEN --api-base URL [--bump minor|patch|major]");
@@ -148,7 +148,7 @@ class VersionBumpRemoteCli extends CliFramework
return 0;
}
private function giteaApi(string $method, string $url, string $token, ?string $body = null): ?array
private function mokogitApi(string $method, string $url, string $token, ?string $body = null): ?array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
@@ -180,7 +180,7 @@ class VersionBumpRemoteCli extends CliFramework
callable $transform,
string $commitMessage
): bool {
$file = $this->giteaApi('GET', "{$apiBase}/contents/{$filePath}?ref={$branch}", $token);
$file = $this->mokogitApi('GET', "{$apiBase}/contents/{$filePath}?ref={$branch}", $token);
if ($file === null || !isset($file['sha']) || !isset($file['content'])) {
return false;
}
@@ -191,7 +191,7 @@ class VersionBumpRemoteCli extends CliFramework
return true;
}
$payload = json_encode(['content' => base64_encode($newContent), 'sha' => $file['sha'], 'message' => $commitMessage, 'branch' => $branch]);
$result = $this->giteaApi('PUT', "{$apiBase}/contents/{$filePath}", $token, $payload);
$result = $this->mokogitApi('PUT', "{$apiBase}/contents/{$filePath}", $token, $payload);
if ($result === null) {
$this->log('ERROR', "{$filePath}: failed to update");
return false;
+2 -25
View File
@@ -38,17 +38,7 @@ class VersionCheckCli extends CliFramework
$root = realpath($path) ?: $path;
$errors = 0;
$versions = [];
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
if (file_exists($mokoManifest)) {
$xml = @simplexml_load_file($mokoManifest);
if ($xml !== false) {
$v = (string)($xml->identity->version ?? '');
$base = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $v);
if (preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $base)) {
$versions['.mokogitea/manifest.xml'] = $base;
}
}
}
// .mokogit/manifest.xml retired — README.md et al. are the version sources now.
$readme = "{$root}/README.md";
if (file_exists($readme)) {
$content = file_get_contents($readme);
@@ -124,19 +114,6 @@ class VersionCheckCli extends CliFramework
file_put_contents($readme, $updated);
} echo " Fixed: README.md -> {$highestVersion}\n";
}
if (isset($versions['.mokogitea/manifest.xml']) && $versions['.mokogitea/manifest.xml'] !== $highestVersion) {
$content = file_get_contents($mokoManifest);
$vPat = '#<version>\d{2}\.\d{2}\.\d{2}'
. '(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#';
$updated = preg_replace(
$vPat,
"<version>{$highestVersion}</version>",
$content
);
if ($updated !== null) {
file_put_contents($mokoManifest, $updated);
} echo " Fixed: .mokogitea/manifest.xml -> {$highestVersion}\n";
}
if (isset($versions['CHANGELOG.md']) && $versions['CHANGELOG.md'] !== $highestVersion) {
$content = file_get_contents($changelog);
$clPat = '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}'
@@ -177,7 +154,7 @@ class VersionCheckCli extends CliFramework
} echo " Fixed: pyproject.toml -> {$highestVersion}\n";
}
foreach ($versions as $source => $ver) {
if (in_array($source, ['README.md', 'CHANGELOG.md', '.mokogitea/manifest.xml', 'package.json', 'pyproject.toml'], true)) {
if (in_array($source, ['README.md', 'CHANGELOG.md', 'package.json', 'pyproject.toml'], true)) {
continue;
} if ($ver === $highestVersion) {
continue;
+4 -49
View File
@@ -32,33 +32,12 @@ class VersionReadCli extends CliFramework
$path = $this->getArgument('--path');
$root = realpath($path) ?: $path;
// -- 1. Read from .mokogitea/manifest.xml (canonical source) --
$mokoVersion = null;
// The retired .mokogit/manifest.xml is no longer a version source; README.md is now
// canonical (version_prefix, when needed, comes from the MokoGit metadata API, not a
// committed manifest). Default to an empty prefix — non-prefixed repos parse normally.
$versionPrefix = '';
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
if (file_exists($mokoManifest)) {
$xml = @simplexml_load_file($mokoManifest);
if ($xml !== false) {
$v = (string)($xml->identity->version ?? '');
if (preg_match('/^\d{2}\.\d{2}\.\d{2}((?:-(?:dev|alpha|beta|rc))+)?$/', $v)) {
$mokoVersion = $v;
}
// Read version_prefix (supports both nested and flat structure)
$prefix = (string)($xml->identity->version_prefix ?? '');
if ($prefix === '') {
$prefix = (string)($xml->version_prefix ?? '');
}
$versionPrefix = $prefix;
}
}
// If manifest.xml has a version, that is authoritative
if ($mokoVersion !== null) {
echo $mokoVersion . "\n";
return 0;
}
// -- 2. Fallback: README.md --
// -- 1. README.md (canonical) --
$readmeVersion = null;
$readme = "{$root}/README.md";
if (file_exists($readme)) {
@@ -151,30 +130,6 @@ class VersionReadCli extends CliFramework
return 1;
}
// -- Backfill: if manifest.xml exists but lacks <version>, insert it --
if (file_exists($mokoManifest)) {
$content = file_get_contents($mokoManifest);
if (!preg_match('#<version>\d{2}\.\d{2}\.\d{2}((?:-(?:dev|alpha|beta|rc))+)?</version>#', $content)) {
if (strpos($content, '<license') !== false) {
$content = preg_replace(
'|(\s*<license)|',
"\n <version>{$version}</version>\$1",
$content,
1
);
} elseif (strpos($content, '</identity>') !== false) {
$content = preg_replace(
'|(</identity>)|',
" <version>{$version}</version>\n \$1",
$content,
1
);
}
file_put_contents($mokoManifest, $content);
$this->log('ERROR', "Backfilled manifest.xml with version {$version}");
}
}
echo $version . "\n";
return 0;
}
+15 -33
View File
@@ -10,22 +10,22 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/version_reset_dev.php
* BRIEF: Reset platform version to 'development' on a branch via Gitea API
* BRIEF: Reset platform version to 'development' on a branch via MokoGit API
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
use MokoCli\{CliFramework, PlatformDetector};
class VersionResetDevCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Reset platform version to development on a branch via Gitea API');
$this->addArgument('--token', 'Gitea API token (also reads MOKOGITEA_TOKEN / GITEA_TOKEN env)', '');
$this->addArgument('--api-base', 'Gitea API base URL for the repo', '');
$this->setDescription('Reset platform version to development on a branch via MokoGit API');
$this->addArgument('--token', 'MokoGit API token (also reads MOKOGIT_TOKEN / MOKOGIT_TOKEN env)', '');
$this->addArgument('--api-base', 'MokoGit API base URL for the repo', '');
$this->addArgument('--branch', 'Target branch (default: dev)', 'dev');
$this->addArgument('--platform', 'Platform type: dolibarr, crm-module, joomla, waas-component', '');
$this->addArgument('--path', 'Repo root for auto-detecting platform from manifest.xml', '');
@@ -41,13 +41,7 @@ class VersionResetDevCli extends CliFramework
// Allow token from environment
if ($token === '') {
$envToken = getenv('MOKOGITEA_TOKEN');
if ($envToken !== false && $envToken !== '') {
$token = $envToken;
}
}
if ($token === '') {
$envToken = getenv('GITEA_TOKEN');
$envToken = getenv('MOKOGIT_TOKEN');
if ($envToken !== false && $envToken !== '') {
$token = $envToken;
}
@@ -93,28 +87,16 @@ class VersionResetDevCli extends CliFramework
private function detectPlatform(string $repoPath): ?string
{
$root = realpath($repoPath) ?: $repoPath;
$manifestXml = "{$root}/.mokogitea/manifest.xml";
if (!file_exists($manifestXml)) {
return null;
}
// .mokogit/manifest.xml retired — detect from files and return the stamp vocabulary
// this tool's callers expect (crm-module / waas-component), matching the old
// governance.platform value.
$platform = PlatformDetector::toStampPlatform(PlatformDetector::detect($root));
$xml = @simplexml_load_file($manifestXml);
if ($xml === false) {
return null;
}
if (isset($xml->governance->platform)) {
$platform = (string) $xml->governance->platform;
if ($platform !== '') {
return $platform;
}
}
return null;
return $platform !== 'generic' ? $platform : null;
}
private function giteaApiCall(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
private function mokogitApiCall(string $url, string $token, string $method = 'GET', ?string $body = null): ?array
{
$ch = curl_init($url);
if ($ch === false) {
@@ -161,7 +143,7 @@ class VersionResetDevCli extends CliFramework
{
// Search the repo tree for mod*.class.php files
$treeUrl = "{$apiBase}/git/trees/{$branch}?recursive=true";
$tree = $this->giteaApiCall($treeUrl, $token);
$tree = $this->mokogitApiCall($treeUrl, $token);
if ($tree === null || !isset($tree['tree']) || !is_array($tree['tree'])) {
$this->log('ERROR', "Could not read repository tree for branch '{$branch}'.");
@@ -191,7 +173,7 @@ class VersionResetDevCli extends CliFramework
// GET file contents via API
$encodedPath = implode('/', array_map('rawurlencode', explode('/', $filePath)));
$fileUrl = "{$apiBase}/contents/{$encodedPath}?ref={$branch}";
$fileData = $this->giteaApiCall($fileUrl, $token);
$fileData = $this->mokogitApiCall($fileUrl, $token);
if ($fileData === null || !isset($fileData['content'])) {
echo "Skipping {$filePath}: could not fetch contents.\n";
@@ -233,7 +215,7 @@ class VersionResetDevCli extends CliFramework
]);
$putUrl = "{$apiBase}/contents/{$encodedPath}";
$result = $this->giteaApiCall($putUrl, $token, 'PUT', $putBody);
$result = $this->mokogitApiCall($putUrl, $token, 'PUT', $putBody);
if ($result !== null) {
echo "Reset: {$filePath} -> \$this->version = 'development'\n";
+4 -29
View File
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
use MokoCli\{CliFramework, SourceResolver, PlatformDetector};
class VersionSetPlatformCli extends CliFramework
{
@@ -77,34 +77,9 @@ class VersionSetPlatformCli extends CliFramework
$root = realpath($path) ?: $path;
// Detect platform — check manifest.xml first, then legacy .mokostandards
$platform = '';
// New format: .mokogitea/manifest.xml (XML with <platform> tag)
$manifestXml = "{$root}/.mokogitea/manifest.xml";
if (file_exists($manifestXml)) {
$xml = @simplexml_load_file($manifestXml);
if ($xml && isset($xml->governance->platform)) {
$platform = (string) $xml->governance->platform;
}
}
// Legacy: .mokostandards YAML file
if (empty($platform)) {
$mokoStandards = "{$root}/.github/.mokostandards";
if (!file_exists($mokoStandards)) {
$mokoStandards = "{$root}/.mokogitea/manifest.xml";
}
if (!file_exists($mokoStandards)) {
$mokoStandards = "{$root}/.mokostandards";
}
if (file_exists($mokoStandards)) {
$content = file_get_contents($mokoStandards);
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
$platform = trim($m[1], " \t\n\r\"'");
}
}
}
// Detect platform from repo files (.mokogit/manifest.xml retired) and translate to
// the stamp vocabulary this tool branches on below (crm-module / waas-component).
$platform = PlatformDetector::toStampPlatform(PlatformDetector::detect($root));
$changed = 0;
+4 -4
View File
@@ -22,7 +22,7 @@ use MokoCli\CliFramework;
class WikiSyncCli extends CliFramework
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $mokogitUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private string $org = 'MokoConsulting';
private string $sourceRepo = 'mokocli';
@@ -39,7 +39,7 @@ class WikiSyncCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Sync wiki pages from mokocli to template repos');
$this->addArgument('--token', 'Gitea API token (required)', '');
$this->addArgument('--token', 'MokoGit API token (required)', '');
$this->addArgument('--org', 'Organization (default: MokoConsulting)', 'MokoConsulting');
$this->addArgument('--source', 'Source repo (default: mokocli)', 'mokocli');
$this->addArgument('--target', 'Target repo (can repeat)', '');
@@ -195,7 +195,7 @@ class WikiSyncCli extends CliFramework
private function apiGet(string $endpoint): ?array
{
$url = "{$this->giteaUrl}/api/v1{$endpoint}";
$url = "{$this->mokogitUrl}/api/v1{$endpoint}";
$opts = [
'http' => [
'method' => 'GET',
@@ -224,7 +224,7 @@ class WikiSyncCli extends CliFramework
private function apiWrite(string $method, string $endpoint, string $payload): ?array
{
$url = "{$this->giteaUrl}/api/v1{$endpoint}";
$url = "{$this->mokogitUrl}/api/v1{$endpoint}";
$opts = [
'http' => [
'method' => $method,
+551 -117
View File
@@ -10,8 +10,9 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/workflow_sync.php
* VERSION: 09.38.05
* BRIEF: Sync workflows from Generic platform templates live repos based on manifest.platform
* VERSION: 09.39.00
* BRIEF: Sync workflows from Generic platform templates live repos based on manifest.platform,
* plus a read-only health-check (--phase health) that alerts on repos missing synced workflows.
*/
declare(strict_types=1);
@@ -24,22 +25,66 @@ class WorkflowSyncCli extends CliFramework
{
private const PLATFORM_TEMPLATES = [
'joomla' => 'Template-Joomla',
'dolibarr' => 'Template-Dolibarr',
'go' => 'Template-Go',
'mcp' => 'Template-MCP',
// Template-MCP was renamed to Template-NPM. Both the 'mcp' platform (declared by
// the mcp-* repos in metadata) and 'npm'/'node' resolve to it. Pointing 'mcp' at the
// old name yielded a 301 → null → Generic fallback, which made cleanupOrphanedWorkflows
// strip every MCP-specific workflow as an "orphan". Keep this in sync with the org.
'mcp' => 'Template-NPM',
'npm' => 'Template-NPM',
'node' => 'Template-NPM',
'platform' => 'Template-Generic',
'generic' => 'Template-Generic',
// Dot-prefixed org repos (.mokogit, .vault, …) sync from Template-Dot. These repos
// carry no platform in metadata, so getRepoPlatform() maps them to 'dot' by name.
'dot' => 'Template-Dot',
// NOTE: 'dolibarr' intentionally omitted — Template-Dolibarr no longer exists and no
// repo declares platform=dolibarr; it falls through to DEFAULT_TEMPLATE (Generic).
];
private const DEFAULT_TEMPLATE = 'Template-Generic';
private const GENERIC_TEMPLATE = 'Template-Generic';
/**
* Canonical workflow directory in every repo.
*
* NOTE: This moved from '.mokogit/workflows' to '.mokogit/workflows' during the
* MokoGit MokoGIT rebrand. The old constant was hardcoded inline in every path,
* so when the repos migrated the sync silently read/wrote a directory that no longer
* existed Phase 1/2 both saw "0 workflows" and did nothing. Keep this in ONE place.
*/
private const WORKFLOW_DIR = '.mokogit/workflows';
/** Default ntfy endpoint for critical drift alerts (matches notify.yml / gitleaks.yml). */
private const DEFAULT_NTFY_URL = 'https://ntfy.mokoconsulting.tech';
private const DEFAULT_NTFY_TOPIC = 'mokogit-ops';
/**
* Workflows to exclude per platform during sync.
* Key = platform name (matching PLATFORM_TEMPLATES keys), Value = array of workflow filenames to skip.
*/
private const PLATFORM_EXCLUDES = [
'joomla' => ['deploy-manual.yml'],
// Dot-repos (.mokogit, .vault, …) are config/data repos: no build, no versioned
// releases, no deploy. Curate Template-Dot down to governance/security/hygiene only by
// excluding the build, versioning, release-pipeline, and deploy workflows. Applied in
// both Phase 1 (Generic → Template-Dot) and Phase 2 (Template-Dot → dot-repos).
'dot' => [
// build/test
'ci-generic.yml',
// versioning
'auto-bump.yml',
'version-set.yml',
// release pipeline
'auto-release.yml',
'pre-release.yml',
'rc-revert.yml',
'cascade-dev.yml',
// deploy (never in Generic, excluded defensively)
'deploy-dev.yml',
'deploy-prod.yml',
'deploy-rc.yml',
],
];
private int $updated = 0;
@@ -50,17 +95,24 @@ class WorkflowSyncCli extends CliFramework
protected function configure(): void
{
$this->setDescription('Sync workflows from Generic → platform templates → live repos based on manifest.platform');
$this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--mokogit-url', 'MokoGit URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'MokoGit API token', '');
$this->addArgument('--org', 'Target organization', '');
$this->addArgument('--branch', 'Target branch (default: main)', 'main');
$this->addArgument('--phase', 'Phase to run: all, templates, repos (default: all)', 'all');
$this->addArgument('--phase', 'Phase to run: all, templates, repos, health (default: all)', 'all');
$this->addArgument('--platform-filter', 'Only sync repos matching this platform', '');
$this->addArgument('--repo-filter', 'Only sync/check these repos (comma-separated names)', '');
// Health-check (--phase health) options — all read-only unless an alert flag is set.
$this->addArgument('--alert-ntfy', 'On drift, send a critical ntfy push notification', false);
$this->addArgument('--ntfy-url', 'ntfy server URL', self::DEFAULT_NTFY_URL);
$this->addArgument('--ntfy-topic', 'ntfy topic', self::DEFAULT_NTFY_TOPIC);
$this->addArgument('--alert-issue', 'On drift, open/update a MokoGit issue on each affected repo', false);
}
protected function run(): int
{
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$mokogitUrl = rtrim($this->getArgument('--mokogit-url'), '/');
$token = $this->getArgument('--token');
$org = $this->getArgument('--org');
$branch = $this->getArgument('--branch');
@@ -77,11 +129,16 @@ class WorkflowSyncCli extends CliFramework
return 1;
}
if (!in_array($phase, ['all', 'templates', 'repos'], true)) {
$this->log('ERROR', "--phase must be one of: all, templates, repos (got: {$phase})");
if (!in_array($phase, ['all', 'templates', 'repos', 'health'], true)) {
$this->log('ERROR', "--phase must be one of: all, templates, repos, health (got: {$phase})");
return 1;
}
// Health-check is a standalone, read-only phase — it never syncs.
if ($phase === 'health') {
return $this->runHealthCheck($mokogitUrl, $token, $org, $branch, $platformFilter);
}
$this->log('INFO', "Workflow Sync — org: {$org}, branch: {$branch}, phase: {$phase}");
if ($platformFilter !== '') {
@@ -96,7 +153,7 @@ class WorkflowSyncCli extends CliFramework
// Phase 1: Sync Generic → Platform Templates
if ($phase === 'all' || $phase === 'templates') {
$result = $this->syncGenericToTemplates($giteaUrl, $token, $org, $branch, $platformFilter);
$result = $this->syncGenericToTemplates($mokogitUrl, $token, $org, $branch, $platformFilter);
if ($result !== 0) {
return $result;
@@ -105,7 +162,7 @@ class WorkflowSyncCli extends CliFramework
// Phase 2: Sync Platform Templates → Live Repos
if ($phase === 'all' || $phase === 'repos') {
$result = $this->syncTemplatesToRepos($giteaUrl, $token, $org, $branch, $platformFilter);
$result = $this->syncTemplatesToRepos($mokogitUrl, $token, $org, $branch, $platformFilter);
if ($result !== 0) {
return $result;
@@ -124,7 +181,7 @@ class WorkflowSyncCli extends CliFramework
* Skips platform-specific overrides (files that exist in the platform template but NOT in Generic).
*/
private function syncGenericToTemplates(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $org,
string $branch,
@@ -134,7 +191,7 @@ class WorkflowSyncCli extends CliFramework
echo "\n";
// Get all workflow files from Template-Generic
$genericWorkflows = $this->listWorkflows($giteaUrl, $token, $org, self::GENERIC_TEMPLATE, $branch);
$genericWorkflows = $this->listWorkflows($mokogitUrl, $token, $org, self::GENERIC_TEMPLATE, $branch);
if ($genericWorkflows === null) {
$this->log('ERROR', 'Could not list workflows from ' . self::GENERIC_TEMPLATE);
@@ -180,12 +237,12 @@ class WorkflowSyncCli extends CliFramework
$this->skipped++;
continue;
}
$destPath = '.mokogitea/workflows/' . $filename;
$destPath = self::WORKFLOW_DIR . '/' . $filename;
$label = "{$templateRepo}/{$filename}";
// Get file content from Generic
$sourceContent = $this->getFileContent(
$giteaUrl, $token, $org,
$mokogitUrl, $token, $org,
self::GENERIC_TEMPLATE, $destPath, $branch
);
@@ -198,7 +255,7 @@ class WorkflowSyncCli extends CliFramework
$commitMsg = "chore: sync {$filename} from " . self::GENERIC_TEMPLATE . " [skip ci]";
$this->pushFile(
$giteaUrl, $token, $org, $templateRepo,
$mokogitUrl, $token, $org, $templateRepo,
$destPath, $sourceContent, $branch, $commitMsg, $label
);
}
@@ -212,7 +269,7 @@ class WorkflowSyncCli extends CliFramework
* Phase 2: Sync platform template workflows to live repos based on manifest.platform.
*/
private function syncTemplatesToRepos(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $org,
string $branch,
@@ -221,7 +278,13 @@ class WorkflowSyncCli extends CliFramework
$this->log('INFO', '=== Phase 2: Sync Platform Templates → Live Repos ===');
echo "\n";
$repos = $this->fetchOrgRepos($giteaUrl, $token, $org);
$repoFilter = $this->parseRepoFilter();
if ($repoFilter !== []) {
$this->log('INFO', 'Repo filter: ' . implode(', ', $repoFilter));
}
$repos = $this->fetchOrgRepos($mokogitUrl, $token, $org);
if ($repos === null) {
return 1;
@@ -244,8 +307,13 @@ class WorkflowSyncCli extends CliFramework
continue;
}
// Apply repo filter
if ($repoFilter !== [] && !in_array($repoName, $repoFilter, true)) {
continue;
}
// Read manifest.platform
$platform = $this->getRepoPlatform($giteaUrl, $token, $org, $repoName, $branch);
$platform = $this->getRepoPlatform($mokogitUrl, $token, $org, $repoName, $branch);
// Apply platform filter
if ($platformFilter !== '' && $platform !== $platformFilter) {
@@ -257,36 +325,44 @@ class WorkflowSyncCli extends CliFramework
// Get workflows from the template (cached)
if (!isset($templateWorkflowCache[$templateRepo])) {
$workflows = $this->listWorkflows($giteaUrl, $token, $org, $templateRepo, $branch);
if ($workflows === null) {
$this->log('WARN', "Could not list workflows from {$templateRepo}, falling back to " . self::GENERIC_TEMPLATE);
$workflows = $this->listWorkflows($giteaUrl, $token, $org, self::GENERIC_TEMPLATE, $branch);
}
$templateWorkflowCache[$templateRepo] = $workflows ?? [];
$templateWorkflowCache[$templateRepo] =
$this->listWorkflows($mokogitUrl, $token, $org, $templateRepo, $branch);
}
$workflows = $templateWorkflowCache[$templateRepo];
// SAFETY: if the mapped template can't be listed (renamed/missing repo — e.g. a stale
// PLATFORM_TEMPLATES entry or a not-yet-created template), do NOT fall back to Generic.
// Falling back would treat Generic's set as authoritative and make
// cleanupOrphanedWorkflows() strip this repo's platform-specific workflows. Skip the
// repo and warn so the miss is visible instead of silently destructive.
if ($workflows === null) {
$this->log('WARN', "Template '{$templateRepo}' for platform '{$platform}' is unreachable"
. " — skipping {$repoName} (no sync, no cleanup).");
continue;
}
if (count($workflows) === 0) {
continue;
}
$templateFilenames = [];
foreach ($workflows as $workflow) {
$filename = $workflow['name'];
$templateFilenames[] = $filename;
$label = "{$repoFullName}/{$filename}";
// Skip platform-excluded workflows
if (in_array($filename, self::PLATFORM_EXCLUDES[$platform] ?? [], true)) {
fprintf(STDERR, "%-45s | %s\n", $label, 'EXCLUDED (platform)');
$this->skipped++;
continue;
}
$destPath = '.mokogitea/workflows/' . $filename;
$label = "{$repoFullName}/{$filename}";
$destPath = self::WORKFLOW_DIR . '/' . $filename;
// Get source content from template
$sourceContent = $this->getFileContent(
$giteaUrl, $token, $org,
$mokogitUrl, $token, $org,
$templateRepo, $destPath, $branch
);
@@ -299,21 +375,136 @@ class WorkflowSyncCli extends CliFramework
$commitMsg = "chore: sync {$filename} from {$templateRepo} [skip ci]";
$this->pushFile(
$giteaUrl, $token, $org, $repoName,
$mokogitUrl, $token, $org, $repoName,
$destPath, $sourceContent, $branch, $commitMsg, $label
);
}
// Cleanup: delete workflows not in the template and not in custom/
$this->cleanupOrphanedWorkflows(
$mokogitUrl, $token, $org, $repoName, $branch,
$templateFilenames, $repoFullName
);
}
echo "\n";
return 0;
}
private const CUSTOM_PREFIX = 'custom/';
/**
* Parse --repo-filter into a list of repo names (empty no filter, process all).
*
* @return string[]
*/
private function parseRepoFilter(): array
{
$raw = (string) $this->getArgument('--repo-filter');
return array_values(array_filter(array_map('trim', explode(',', $raw)), fn($n) => $n !== ''));
}
/**
* Delete workflows in a repo that are not in the template and not under custom/.
*/
private function cleanupOrphanedWorkflows(
string $mokogitUrl,
string $token,
string $org,
string $repoName,
string $branch,
array $templateFilenames,
string $repoFullName
): void {
$repoWorkflows = $this->listWorkflows($mokogitUrl, $token, $org, $repoName, $branch);
if ($repoWorkflows === null) {
return;
}
foreach ($repoWorkflows as $item) {
$name = $item['name'] ?? '';
$type = $item['type'] ?? 'file';
// Skip directories (like custom/)
if ($type !== 'file') {
continue;
}
// Skip files that exist in the template
if (in_array($name, $templateFilenames, true)) {
continue;
}
// This file is not in the template — delete it
$destPath = self::WORKFLOW_DIR . '/' . $name;
$label = "{$repoFullName}/{$name}";
$this->deleteFile(
$mokogitUrl, $token, $org, $repoName,
$destPath, $branch,
"chore: remove orphaned workflow {$name} [skip ci]",
$label
);
}
}
/**
* Delete a file from a repo via the MokoGit API.
*/
private function deleteFile(
string $mokogitUrl,
string $token,
string $org,
string $repoName,
string $filePath,
string $branch,
string $commitMsg,
string $label
): void {
$existing = $this->apiRequest(
$mokogitUrl, $token, 'GET',
"/api/v1/repos/{$org}/{$repoName}/contents/{$filePath}?ref={$branch}"
);
if ($existing['code'] !== 200) {
return;
}
$data = json_decode($existing['body'], true);
$sha = $data['sha'] ?? '';
if ($this->dryRun) {
fprintf(STDERR, "%-45s | %s\n", $label, 'WOULD DELETE');
return;
}
$payload = json_encode([
'sha' => $sha,
'message' => $commitMsg,
'branch' => $branch,
]);
$response = $this->apiRequest(
$mokogitUrl, $token, 'DELETE',
"/api/v1/repos/{$org}/{$repoName}/contents/{$filePath}",
$payload
);
if ($response['code'] === 200) {
fprintf(STDERR, "%-45s | %s\n", $label, 'DELETED (orphaned)');
} else {
fprintf(STDERR, "%-45s | %s\n", $label, "DELETE ERROR (HTTP {$response['code']})");
$this->errors++;
}
}
/**
* Push a file to a repo create or update, skip if identical.
*/
private function pushFile(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $org,
string $repoName,
@@ -324,7 +515,7 @@ class WorkflowSyncCli extends CliFramework
string $label
): void {
$existing = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/repos/{$org}/{$repoName}/contents/"
@@ -358,7 +549,7 @@ class WorkflowSyncCli extends CliFramework
]);
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'PUT',
"/api/v1/repos/{$org}/{$repoName}/contents/" . $destPath,
@@ -386,7 +577,7 @@ class WorkflowSyncCli extends CliFramework
]);
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'POST',
"/api/v1/repos/{$org}/{$repoName}/contents/" . $destPath,
@@ -407,20 +598,20 @@ class WorkflowSyncCli extends CliFramework
}
/**
* List workflow files in a repo's .mokogitea/workflows/ directory.
* List workflow files in a repo's .mokogit/workflows/ directory.
*/
private function listWorkflows(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $org,
string $repoName,
string $branch
): ?array {
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/repos/{$org}/{$repoName}/contents/.mokogitea/workflows?ref={$branch}"
"/api/v1/repos/{$org}/{$repoName}/contents/" . self::WORKFLOW_DIR . "?ref={$branch}"
);
if ($response['code'] !== 200) {
@@ -441,7 +632,7 @@ class WorkflowSyncCli extends CliFramework
* Get file content from a repo as a raw string.
*/
private function getFileContent(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $org,
string $repoName,
@@ -449,7 +640,7 @@ class WorkflowSyncCli extends CliFramework
string $branch
): ?string {
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/repos/{$org}/{$repoName}/contents/{$filePath}?ref={$branch}"
@@ -469,21 +660,29 @@ class WorkflowSyncCli extends CliFramework
}
/**
* Read a repo's manifest.xml and extract the platform value.
* Returns 'generic' if the manifest is missing or has no platform field.
* Read a repo's platform from the MokoGit metadata API.
* Returns 'generic' if metadata is missing or has no platform field.
* (Replaces the retired .mokogit/manifest.xml lookup platform now lives
* in the repo Metadata endpoint: GET /api/v1/repos/{owner}/{repo}/metadata.)
*/
private function getRepoPlatform(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $org,
string $repoName,
string $branch
): string {
// Dot-prefixed org repos (.mokogit, .vault, …) sync from Template-Dot regardless of
// metadata (they carry no platform field). Name is authoritative for these.
if (str_starts_with($repoName, '.')) {
return 'dot';
}
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/repos/{$org}/{$repoName}/contents/.mokogitea/manifest.xml?ref={$branch}"
"/api/v1/repos/{$org}/{$repoName}/metadata"
);
if ($response['code'] !== 200) {
@@ -492,80 +691,17 @@ class WorkflowSyncCli extends CliFramework
$data = json_decode($response['body'], true);
if (!is_array($data) || !isset($data['content'])) {
if (!is_array($data) || empty($data['platform'])) {
return 'generic';
}
$xmlContent = base64_decode($data['content']);
if ($xmlContent === false || $xmlContent === '') {
return 'generic';
}
// Suppress XML warnings for malformed manifests
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string($xmlContent);
libxml_use_internal_errors($previous);
if ($xml === false) {
return 'generic';
}
// Try <governance><platform> (standard location)
$platform = '';
// Register namespace if present
$namespaces = $xml->getNamespaces(true);
if (!empty($namespaces)) {
$ns = reset($namespaces);
$xml->registerXPathNamespace('mp', $ns);
$nodes = $xml->xpath('//mp:governance/mp:platform');
if (!empty($nodes)) {
$platform = trim((string) $nodes[0]);
}
// Fallback: <identity><platform>
if ($platform === '') {
$nodes = $xml->xpath('//mp:identity/mp:platform');
if (!empty($nodes)) {
$platform = trim((string) $nodes[0]);
}
}
// Fallback: top-level <platform>
if ($platform === '') {
$nodes = $xml->xpath('//mp:platform');
if (!empty($nodes)) {
$platform = trim((string) $nodes[0]);
}
}
} else {
// No namespace
if (isset($xml->governance->platform)) {
$platform = trim((string) $xml->governance->platform);
} elseif (isset($xml->identity->platform)) {
$platform = trim((string) $xml->identity->platform);
} elseif (isset($xml->platform)) {
$platform = trim((string) $xml->platform);
}
}
if ($platform === '') {
return 'generic';
}
return strtolower($platform);
return strtolower(trim((string) $data['platform']));
}
/**
* Fetch all non-archived repos in an org (paginated).
*/
private function fetchOrgRepos(string $giteaUrl, string $token, string $org): ?array
private function fetchOrgRepos(string $mokogitUrl, string $token, string $org): ?array
{
$this->log('INFO', "Fetching repos from org: {$org}");
@@ -574,7 +710,7 @@ class WorkflowSyncCli extends CliFramework
while (true) {
$response = $this->apiRequest(
$giteaUrl,
$mokogitUrl,
$token,
'GET',
"/api/v1/orgs/{$org}/repos?"
@@ -615,17 +751,315 @@ class WorkflowSyncCli extends CliFramework
return $repos;
}
// =========================================================================
// Phase: health — read-only drift detection + alerting
// =========================================================================
private const HEALTH_ISSUE_MARKER = '[workflow-health]';
/**
* Make an HTTP request to the Gitea API.
* Health-check phase: verify every live repo still has the synced workflows its
* platform template provides, and (optionally) alert on drift.
*
* This closes the exact gap that caused the original outage: the sync treated an
* empty/missing workflow directory as "nothing to do" and moved on silently. Here
* we assert the canonical set is present and fail loudly (non-zero exit) when it
* is not so a scheduled MokoGit Action, an ntfy push, and/or a MokoGit issue surface
* the problem instead of it rotting undetected.
*/
private function runHealthCheck(
string $mokogitUrl,
string $token,
string $org,
string $branch,
string $platformFilter
): int {
$alertNtfy = (bool) $this->getArgument('--alert-ntfy');
$alertIssue = (bool) $this->getArgument('--alert-issue');
$ntfyUrl = rtrim((string) $this->getArgument('--ntfy-url'), '/');
$ntfyTopic = (string) $this->getArgument('--ntfy-topic');
$this->log('INFO', "Workflow Health-Check — org: {$org}, branch: {$branch}");
if ($platformFilter !== '') {
$this->log('INFO', "Platform filter: {$platformFilter}");
}
$repoFilter = $this->parseRepoFilter();
if ($repoFilter !== []) {
$this->log('INFO', 'Repo filter: ' . implode(', ', $repoFilter));
}
echo "\n";
$repos = $this->fetchOrgRepos($mokogitUrl, $token, $org);
if ($repos === null) {
return self::EXIT_FAILURE;
}
$this->log('INFO', 'Scanning ' . count($repos) . " repo(s) in \"{$org}\".");
echo "\n";
$templateCache = [];
$unhealthy = [];
$checked = 0;
foreach ($repos as $repoFullName) {
[, $repoName] = explode('/', $repoFullName, 2);
if (str_starts_with($repoName, 'Template-')) {
continue;
}
$platform = $this->getRepoPlatform($mokogitUrl, $token, $org, $repoName, $branch);
if ($platformFilter !== '' && $platform !== $platformFilter) {
continue;
}
if ($repoFilter !== [] && !in_array($repoName, $repoFilter, true)) {
continue;
}
// A repo that has no such branch is simply out of scope for this run
// (e.g. single-branch repos have no 'dev'), not unhealthy.
if (!$this->branchExists($mokogitUrl, $token, $org, $repoName, $branch)) {
continue;
}
$templateRepo = self::PLATFORM_TEMPLATES[$platform] ?? self::DEFAULT_TEMPLATE;
// Expected set = the template's workflows (cached), minus platform excludes.
// Do NOT fall back to Generic when the mapped template is unreachable — that would
// silently assess against the wrong baseline. Record null (missing) distinctly.
if (!array_key_exists($templateRepo, $templateCache)) {
$tpl = $this->listWorkflows($mokogitUrl, $token, $org, $templateRepo, $branch);
$templateCache[$templateRepo] = $tpl === null
? null
: array_map(fn($w) => $w['name'], $tpl);
}
if ($templateCache[$templateRepo] === null) {
$this->log('WARN', "Template '{$templateRepo}' for platform '{$platform}' is unreachable"
. " — cannot assess {$repoName}.");
continue;
}
$expected = array_values(array_diff(
$templateCache[$templateRepo],
self::PLATFORM_EXCLUDES[$platform] ?? []
));
// Template itself is empty — we can't assert anything meaningful.
if (count($expected) === 0) {
continue;
}
$repoList = $this->listWorkflows($mokogitUrl, $token, $org, $repoName, $branch);
$actual = array_map(fn($w) => $w['name'], $repoList ?? []);
$missing = $this->evaluateRepoHealth($expected, $actual);
$checked++;
if (count($missing) > 0) {
$unhealthy[$repoName] = [
'full' => $repoFullName,
'platform' => $platform,
'expected' => count($expected),
'present' => count(array_intersect($expected, $actual)),
'missing' => $missing,
];
}
}
echo "\n";
if (count($unhealthy) === 0) {
$this->success("All {$checked} in-scope repo(s) have their synced workflows on '{$branch}'.");
return self::EXIT_SUCCESS;
}
$rows = [];
foreach ($unhealthy as $repo => $info) {
$rows[] = [
$repo,
$info['platform'],
"{$info['present']}/{$info['expected']}",
(string) count($info['missing']),
];
}
$this->log('ERROR', count($unhealthy) . " of {$checked} repo(s) are MISSING synced workflows on '{$branch}':");
$this->table(['Repo', 'Platform', 'Present', 'Missing'], $rows);
if ($alertNtfy) {
$this->sendNtfyAlert($ntfyUrl, $ntfyTopic, $org, $branch, $unhealthy);
}
if ($alertIssue) {
foreach ($unhealthy as $repo => $info) {
$this->openHealthIssue($mokogitUrl, $token, $org, $repo, $branch, $info);
}
}
return self::EXIT_FAILURE;
}
/**
* Health verdict policy given the workflows a repo SHOULD have (its platform
* template's set, minus platform excludes) and the workflows it ACTUALLY has,
* return the filenames that are missing. Non-empty result repo is unhealthy.
*
* Default policy: every expected workflow must be present by exact filename.
* Extra files (repo-specific custom/ workflows, in-flight RC workflows) are
* ignored we assert the canonical set is PRESENT, never that nothing else exists.
*
* @param string[] $expected Canonical workflow filenames the repo should carry.
* @param string[] $actual Workflow filenames currently in the repo.
* @return string[] Missing filenames (empty healthy).
*/
private function evaluateRepoHealth(array $expected, array $actual): array
{
return array_values(array_diff($expected, $actual));
}
/**
* Return true when the given branch exists in the repo.
*/
private function branchExists(
string $mokogitUrl,
string $token,
string $org,
string $repoName,
string $branch
): bool {
$response = $this->apiRequest(
$mokogitUrl, $token, 'GET',
"/api/v1/repos/{$org}/{$repoName}/branches/{$branch}"
);
return $response['code'] === 200;
}
/**
* Send a critical ntfy push notification about drift.
* Matches the org convention in .mokogit/workflows/notify.yml (Title/Priority/Tags headers).
*/
private function sendNtfyAlert(
string $ntfyUrl,
string $topic,
string $org,
string $branch,
array $unhealthy
): void {
if ($ntfyUrl === '' || $topic === '') {
$this->warn('ntfy alert requested but --ntfy-url/--ntfy-topic is empty; skipping.');
return;
}
$count = count($unhealthy);
$names = implode(', ', array_keys($unhealthy));
$title = "Workflow drift: {$count} repo(s) missing workflows";
$body = "Org {$org} ({$branch}): {$count} repo(s) missing synced " . self::WORKFLOW_DIR . "/.\n{$names}";
if ($this->dryRun) {
$this->log('INFO', "[DRY RUN] Would send critical ntfy to {$ntfyUrl}/{$topic}: {$title}");
return;
}
$ch = curl_init("{$ntfyUrl}/{$topic}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Title: {$title}",
'Priority: urgent',
'Tags: rotating_light,warning',
]);
curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300) {
$this->success("Sent critical ntfy alert to {$ntfyUrl}/{$topic}.");
} else {
$this->warn("ntfy alert failed (HTTP {$code}).");
}
}
/**
* Open a MokoGit issue on an affected repo describing the missing workflows.
* Idempotent: skips if an open issue carrying the marker already exists.
*/
private function openHealthIssue(
string $mokogitUrl,
string $token,
string $org,
string $repoName,
string $branch,
array $info
): void {
// Idempotency — don't stack duplicate issues on repeated runs.
$existing = $this->apiRequest(
$mokogitUrl, $token, 'GET',
"/api/v1/repos/{$org}/{$repoName}/issues?state=open&type=issues&limit=50"
);
if ($existing['code'] === 200) {
$issues = json_decode($existing['body'], true) ?: [];
foreach ($issues as $iss) {
if (str_contains((string) ($iss['title'] ?? ''), self::HEALTH_ISSUE_MARKER)) {
$this->log('INFO', "{$repoName}: health issue already open (#{$iss['number']}), skipping.");
return;
}
}
}
$missingList = implode("\n", array_map(fn($f) => "- `{$f}`", $info['missing']));
$title = self::HEALTH_ISSUE_MARKER . " Missing synced workflows on {$branch}";
$body = 'Automated health-check found this repository is missing '
. count($info['missing']) . " workflow(s) that its platform template "
. "(`{$info['platform']}`) provides in `" . self::WORKFLOW_DIR . "/` on `{$branch}`:\n\n"
. $missingList
. "\n\nRestore with:\n\n```\nphp bin/moko workflow:sync --org {$org} --branch {$branch} --token \$TOKEN\n```"
. "\n\n<sub>Filed by workflow_sync.php --phase health.</sub>";
if ($this->dryRun) {
$this->log('INFO', "[DRY RUN] Would open health issue on {$org}/{$repoName}.");
return;
}
$payload = json_encode(['title' => $title, 'body' => $body]);
$resp = $this->apiRequest(
$mokogitUrl, $token, 'POST',
"/api/v1/repos/{$org}/{$repoName}/issues",
$payload
);
if ($resp['code'] === 201) {
$data = json_decode($resp['body'], true);
$this->success("Opened health issue {$org}/{$repoName}#" . ($data['number'] ?? '?') . '.');
} else {
$this->warn("Failed to open issue on {$org}/{$repoName} (HTTP {$resp['code']}).");
}
}
/**
* Make an HTTP request to the MokoGit API.
*/
private function apiRequest(
string $giteaUrl,
string $mokogitUrl,
string $token,
string $method,
string $endpoint,
?string $body = null
): array {
$url = $giteaUrl . $endpoint;
$url = $mokogitUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
+1 -1
View File
@@ -61,7 +61,7 @@ Shared library code
- GitHub client
### [Wiki](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki)
All documentation lives in the Gitea wiki.
All documentation lives in the MokoGit wiki.
### [Tests](tests/)
Test scripts
+1 -1
View File
@@ -154,7 +154,7 @@ class ApiClient
* @param bool $enableCaching Enable response caching
* @param string $userAgent User agent string
* @param LoggerInterface|null $logger Optional logger
* @param string $authScheme Authorization scheme ('Bearer' for GitHub, 'token' for Gitea)
* @param string $authScheme Authorization scheme ('Bearer' for GitHub, 'token' for MokoGit)
*/
public function __construct(
string $baseUrl,
+10 -10
View File
@@ -74,14 +74,14 @@ class Config
private const DEFAULT_CONFIG = [
'version' => '04.00.04',
'environment' => 'development',
'platform' => 'gitea',
'platform' => 'mokogit',
'github' => [
'organization' => 'mokoconsulting-tech',
'rate_limit' => 5000,
'max_retries' => 3,
'timeout' => 30,
],
'gitea' => [
'mokogit' => [
'url' => 'https://git.mokoconsulting.tech',
'organization' => 'mokoconsulting-tech',
'rate_limit' => 5000,
@@ -227,16 +227,16 @@ class Config
$configData['github']['organization'] = $org;
}
// Gitea token resolution: GA_TOKEN env var (Gitea Actions)
$giteaToken = getenv('GA_TOKEN') ?: '';
if (!empty($giteaToken)) {
$configData['gitea']['token'] = $giteaToken;
// MokoGit token resolution: GA_TOKEN env var (MokoGit Actions)
$mokogitToken = getenv('GA_TOKEN') ?: '';
if (!empty($mokogitToken)) {
$configData['mokogit']['token'] = $mokogitToken;
}
if ($giteaUrl = getenv('GITEA_URL')) {
$configData['gitea']['url'] = rtrim($giteaUrl, '/');
if ($mokogitUrl = getenv('MOKOGIT_URL')) {
$configData['mokogit']['url'] = rtrim($mokogitUrl, '/');
}
if ($giteaOrg = getenv('GITEA_ORG')) {
$configData['gitea']['organization'] = $giteaOrg;
if ($mokogitOrg = getenv('MOKOGIT_ORG')) {
$configData['mokogit']['organization'] = $mokogitOrg;
}
// Logging configuration
+1 -1
View File
@@ -419,7 +419,7 @@ class GitHubAdapter implements GitPlatformAdapter
public function migrateRepository(array $options): array
{
throw new RuntimeException('Repository migration is not supported on GitHub — use Gitea\'s built-in migration');
throw new RuntimeException('Repository migration is not supported on GitHub — use MokoGit\'s built-in migration');
}
// ──────────────────────────────────────────────
+10 -10
View File
@@ -11,7 +11,7 @@
* INGROUP: MokoPlatform.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /lib/Enterprise/GitPlatformAdapter.php
* BRIEF: Interface defining all git platform operations for GitHub/Gitea abstraction
* BRIEF: Interface defining all git platform operations for GitHub/MokoGit abstraction
*/
declare(strict_types=1);
@@ -22,7 +22,7 @@ namespace MokoCli;
* Git Platform Adapter Interface
*
* Defines all platform operations required by mokocli automation.
* Implementations exist for GitHub (GitHubAdapter) and Gitea (MokoGiteaAdapter),
* Implementations exist for GitHub (GitHubAdapter) and MokoGit (MokoGitAdapter),
* allowing scripts to work against either platform transparently.
*
* @package MokoPlatform\Enterprise
@@ -37,7 +37,7 @@ interface GitPlatformAdapter
/**
* Get the platform name identifier.
*
* @return string 'github' or 'gitea'
* @return string 'github' or 'mokogit'
*/
public function getPlatformName(): string;
@@ -51,14 +51,14 @@ interface GitPlatformAdapter
/**
* Get the workflow directory name for this platform.
*
* @return string '.github/workflows' or '.mokogitea/workflows'
* @return string '.github/workflows' or '.mokogit/workflows'
*/
public function getWorkflowDir(): string;
/**
* Get the platform-specific metadata directory.
*
* @return string '.github' or '.mokogitea'
* @return string '.github' or '.mokogit'
*/
public function getMetadataDir(): string;
@@ -104,7 +104,7 @@ interface GitPlatformAdapter
/**
* Get the environment variable name for step summary output (CI-specific).
*
* @return string 'GITHUB_STEP_SUMMARY' or 'GITEA_STEP_SUMMARY'
* @return string 'GITHUB_STEP_SUMMARY' or 'MOKOGIT_STEP_SUMMARY'
*/
public function getStepSummaryEnvVar(): string;
@@ -379,7 +379,7 @@ interface GitPlatformAdapter
* @param string $org Organization name
* @param string $repo Repository name
* @param int $number Issue/PR number
* @param array<string> $labels Label names (GitHub) or label IDs (Gitea)
* @param array<string> $labels Label names (GitHub) or label IDs (MokoGit)
* @return array<string, mixed> API response
*/
public function addIssueLabels(string $org, string $repo, int $number, array $labels): array;
@@ -391,7 +391,7 @@ interface GitPlatformAdapter
/**
* Set branch protection rules.
*
* On GitHub this maps to rulesets; on Gitea to branch_protections.
* On GitHub this maps to rulesets; on MokoGit to branch_protections.
*
* @param string $org Organization name
* @param string $repo Repository name
@@ -450,13 +450,13 @@ interface GitPlatformAdapter
public function paginateAll(string $endpoint, array $params = [], int $perPage = 100): array;
// ──────────────────────────────────────────────
// Migration (Gitea-specific, no-op on GitHub)
// Migration (MokoGit-specific, no-op on GitHub)
// ──────────────────────────────────────────────
/**
* Migrate a repository from an external service.
*
* On Gitea, this calls POST /api/v1/repos/migrate.
* On MokoGit, this calls POST /api/v1/repos/migrate.
* On GitHub, this is a no-op (throws UnsupportedOperationException).
*
* @param array<string, mixed> $options Migration options (clone_addr, service, auth_token, etc.)
+1 -1
View File
@@ -26,7 +26,7 @@ use SimpleXMLElement;
* MokoCli Parser
*
* Reads, writes, and validates the manifest.xml repository manifest.
* The file uses XML format (no file extension) and lives at .mokogitea/manifest.xml.
* The file uses XML format (no file extension) and lives at .mokogit/manifest.xml.
*
* @package MokoPlatform\Enterprise
* @version 04.07.00
-194
View File
@@ -1,194 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.Enterprise
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /lib/Enterprise/ManifestReader.php
* BRIEF: Read and parse .mokogitea/manifest.xml shared across all CLI tools
*/
declare(strict_types=1);
namespace MokoCli;
/**
* Manifest Reader
*
* Parses .mokogitea/manifest.xml and provides typed access to all fields.
* Used by CLI tools and the Enterprise library to determine platform,
* build configuration, and deployment settings from the repository manifest.
*
* @since 09.01.00
*/
class ManifestReader
{
/** @var array<string, string> Parsed manifest fields */
private array $fields = [];
/** @var bool Whether a manifest was found and parsed */
private bool $loaded = false;
/**
* Load manifest from a repository root directory.
*
* @param string $root Repository root path
* @return self
*/
public static function fromPath(string $root): self
{
$reader = new self();
$reader->load($root);
return $reader;
}
/**
* Load and parse the manifest file.
*
* @param string $root Repository root path
*/
public function load(string $root): void
{
$candidates = [
"{$root}/.mokogitea/manifest.xml",
"{$root}/.mokogitea/.manifest.xml",
"{$root}/.mokogitea/.mokocli",
];
$manifestFile = null;
foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
$manifestFile = $candidate;
break;
}
}
if ($manifestFile === null) {
return;
}
$xml = @simplexml_load_file($manifestFile);
if ($xml === false) {
// Fallback: YAML legacy format
$content = file_get_contents($manifestFile);
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
$this->fields['platform'] = trim($m[1], " \t\n\r\"'");
}
$this->loaded = true;
return;
}
$this->fields = [
'name' => (string)($xml->identity->name ?? ''),
'org' => (string)($xml->identity->org ?? ''),
'description' => (string)($xml->identity->description ?? ''),
'license' => (string)($xml->identity->license ?? ''),
'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''),
'version' => (string)($xml->identity->version ?? ''),
'platform' => (string)($xml->governance->platform ?? ''),
'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''),
'language' => (string)($xml->build->language ?? ''),
'package-type' => (string)($xml->build->{"package-type"} ?? ''),
'entry-point' => (string)($xml->build->{"entry-point"} ?? ''),
'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''),
'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''),
'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''),
'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''),
];
// Strip empty values
$this->fields = array_filter($this->fields, fn($v) => $v !== '');
$this->loaded = true;
}
/**
* Whether a manifest was found and loaded.
*
* @return bool
*/
public function isLoaded(): bool
{
return $this->loaded;
}
/**
* Get a single field value.
*
* @param string $key Field name (e.g. 'platform', 'package-type')
* @param string $default Default value if field is missing
* @return string
*/
public function get(string $key, string $default = ''): string
{
return $this->fields[$key] ?? $default;
}
/**
* Get the platform slug, normalized to canonical values.
*
* @return string One of: joomla, dolibarr, generic, mcp, nodejs
*/
public function getPlatform(): string
{
$raw = $this->get('platform', 'generic');
return match ($raw) {
'waas-component' => 'joomla',
'crm-module' => 'dolibarr',
default => $raw,
};
}
/**
* Get the source/entry-point directory.
*
* Fallback chain: manifest entry-point source/ src/ htdocs/ 'source'.
* Uses SourceResolver for the directory fallback when no entry-point is set.
*
* @param string $root Repository root for existence checking
* @return string Resolved source directory path (e.g. 'source', 'src', 'htdocs')
*/
public function getSourceDir(string $root = ''): string
{
$entryPoint = $this->get('entry-point', '');
if ($entryPoint !== '') {
// Strip trailing filename (e.g. source/index.ts → source)
$dir = rtrim(dirname($entryPoint) === '.' ? $entryPoint : dirname($entryPoint), '/');
if ($root === '' || is_dir("{$root}/{$dir}")) {
return $dir;
}
}
// Fallback: use SourceResolver (source/ → src/ → htdocs/ → default 'source')
if ($root !== '') {
return SourceResolver::resolve($root);
}
return 'source';
}
/**
* Get the package type for build decisions.
*
* @return string e.g. 'package', 'dolibarr', 'generic', 'mcp-server'
*/
public function getPackageType(): string
{
return $this->get('package-type', 'generic');
}
/**
* Get all parsed fields.
*
* @return array<string, string>
*/
public function getAll(): array
{
return $this->fields;
}
}
@@ -10,8 +10,8 @@
* DEFGROUP: MokoPlatform.Enterprise.Platform
* INGROUP: MokoPlatform.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /lib/Enterprise/MokoGiteaAdapter.php
* BRIEF: Gitea implementation of GitPlatformAdapter
* PATH: /lib/Enterprise/MokoGitAdapter.php
* BRIEF: MokoGit implementation of GitPlatformAdapter
*/
declare(strict_types=1);
@@ -22,32 +22,32 @@ use Exception;
use RuntimeException;
/**
* Gitea implementation of GitPlatformAdapter.
* MokoGit implementation of GitPlatformAdapter.
*
* Wraps ApiClient with Gitea-specific API semantics:
* Wraps ApiClient with MokoGit-specific API semantics:
* - Base URL: https://git.mokoconsulting.tech/api/v1
* - Auth: token {token}
* - Pagination: limit + page params
* - File ops: POST for create, PUT for update (check existence first)
* - Topics: PUT with {"topics": [...]}
* - Branch protection: flat API (not rulesets)
* - Workflow dir: .mokogitea/workflows
* - Workflow dir: .mokogit/workflows
*
* @package MokoPlatform\Enterprise
* @since 04.06.10
* @see GitPlatformAdapter
*/
class MokoGiteaAdapter implements GitPlatformAdapter
class MokoGitAdapter implements GitPlatformAdapter
{
/** @var ApiClient HTTP client for Gitea API calls. */
/** @var ApiClient HTTP client for MokoGit API calls. */
private ApiClient $apiClient;
/** @var string Base URL for Gitea API (e.g. https://git.mokoconsulting.tech/api/v1). */
/** @var string Base URL for MokoGit API (e.g. https://git.mokoconsulting.tech/api/v1). */
private string $baseUrl;
/**
* @param ApiClient $apiClient Configured API client
* @param string $baseUrl Gitea API base URL
* @param string $baseUrl MokoGit API base URL
*/
public function __construct(ApiClient $apiClient, string $baseUrl = 'https://git.mokoconsulting.tech/api/v1')
{
@@ -61,7 +61,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function getPlatformName(): string
{
return 'gitea';
return 'mokogit';
}
public function getBaseUrl(): string
@@ -71,12 +71,12 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function getWorkflowDir(): string
{
return '.mokogitea/workflows';
return '.mokogit/workflows';
}
public function getMetadataDir(): string
{
return '.mokogitea';
return '.mokogit';
}
public function getRepoWebUrl(string $org, string $repo): string
@@ -88,7 +88,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function getPullRequestWebUrl(string $org, string $repo, int $number): string
{
// Gitea uses /pulls/ (not /pull/) for web UI
// MokoGit uses /pulls/ (not /pull/) for web UI
$webBase = preg_replace('#/api/v1$#', '', $this->baseUrl);
return "{$webBase}/{$org}/{$repo}/pulls/{$number}";
}
@@ -106,14 +106,14 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function getBranchWebUrl(string $org, string $repo, string $branch): string
{
// Gitea uses /src/branch/ (not /tree/) for web UI
// MokoGit uses /src/branch/ (not /tree/) for web UI
$webBase = preg_replace('#/api/v1$#', '', $this->baseUrl);
return "{$webBase}/{$org}/{$repo}/src/branch/{$branch}";
}
public function getStepSummaryEnvVar(): string
{
return 'GITEA_STEP_SUMMARY';
return 'MOKOGIT_STEP_SUMMARY';
}
// ──────────────────────────────────────────────
@@ -157,7 +157,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function archiveRepo(string $org, string $repo): array
{
// Gitea uses PATCH with archived flag, same as GitHub
// MokoGit uses PATCH with archived flag, same as GitHub
return $this->apiClient->patch("/repos/{$org}/{$repo}", [
'archived' => true,
]);
@@ -165,7 +165,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function setRepoTopics(string $org, string $repo, array $topics): void
{
// Gitea uses {"topics": [...]} not {"names": [...]}
// MokoGit uses {"topics": [...]} not {"names": [...]}
$this->apiClient->put("/repos/{$org}/{$repo}/topics", [
'topics' => $topics,
]);
@@ -209,12 +209,12 @@ class MokoGiteaAdapter implements GitPlatformAdapter
}
if ($sha !== null) {
// Update existing file — Gitea uses PUT with SHA
// Update existing file — MokoGit uses PUT with SHA
$data['sha'] = $sha;
return $this->apiClient->put("/repos/{$org}/{$repo}/contents/{$path}", $data);
}
// Create new file — Gitea uses POST
// Create new file — MokoGit uses POST
return $this->apiClient->post("/repos/{$org}/{$repo}/contents/{$path}", $data);
}
@@ -226,7 +226,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
string $message,
?string $branch = null
): array {
// Gitea's delete uses the same endpoint but with DELETE method
// MokoGit's delete uses the same endpoint but with DELETE method
// ApiClient::delete() doesn't support a body, so we use the raw approach
// For now, this matches GitHubAdapter's limitation
return $this->apiClient->delete("/repos/{$org}/{$repo}/contents/{$path}");
@@ -281,7 +281,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
string $body = '',
array $options = []
): array {
// Gitea expects label IDs (int64), not names. Resolve if needed.
// MokoGit expects label IDs (int64), not names. Resolve if needed.
if (!empty($options['labels']) && is_string($options['labels'][0] ?? null)) {
$labelNames = $options['labels'];
$existing = $this->listLabels($org, $repo);
@@ -330,7 +330,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function createLabel(string $org, string $repo, string $name, string $color, string $description = ''): array
{
// Gitea expects color with # prefix
// MokoGit expects color with # prefix
$color = ltrim($color, '#');
return $this->apiClient->post("/repos/{$org}/{$repo}/labels", [
'name' => $name,
@@ -341,7 +341,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function addIssueLabels(string $org, string $repo, int $number, array $labels): array
{
// Gitea requires label IDs, not names. Resolve names to IDs first.
// MokoGit requires label IDs, not names. Resolve names to IDs first.
$allLabels = $this->listLabels($org, $repo);
$labelMap = [];
foreach ($allLabels as $label) {
@@ -372,7 +372,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function setBranchProtection(string $org, string $repo, string $branch, array $rules): array
{
// Gitea uses a flat branch protection API
// MokoGit uses a flat branch protection API
$whitelistActions = $rules['whitelist_actions_user'] ?? false;
$protection = [
'branch_name' => $branch,
@@ -421,7 +421,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
// Try as a tag first
try {
$tag = $this->apiClient->get("/repos/{$org}/{$repo}/git/tags/{$ref}");
// Gitea tag objects have a 'commit' field with the SHA
// MokoGit tag objects have a 'commit' field with the SHA
if (isset($tag['commit']['sha'])) {
return $tag['commit']['sha'];
}
@@ -458,7 +458,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
{
$all = [];
$page = 1;
// Gitea uses 'limit' instead of 'per_page'
// MokoGit uses 'limit' instead of 'per_page'
$params['limit'] = $perPage;
while (true) {
@@ -488,7 +488,7 @@ class MokoGiteaAdapter implements GitPlatformAdapter
public function migrateRepository(array $options): array
{
// Gitea's built-in migration endpoint
// MokoGit's built-in migration endpoint
$data = array_merge([
'service' => 'github',
'issues' => true,
+29 -36
View File
@@ -44,18 +44,18 @@ class PlatformAdapterFactory
* Create a GitPlatformAdapter based on configuration.
*
* @param Config $config Configuration instance
* @param string|null $platformOverride Force a specific platform ('github' or 'gitea')
* @param string|null $platformOverride Force a specific platform ('github' or 'mokogit')
* @return GitPlatformAdapter The constructed adapter
* @throws RuntimeException If the platform is not supported or token is missing
*/
public static function create(Config $config, ?string $platformOverride = null): GitPlatformAdapter
{
$platform = $platformOverride ?? $config->getString('platform', 'gitea');
$platform = $platformOverride ?? $config->getString('platform', 'mokogit');
return match ($platform) {
'github' => self::createGitHubAdapter($config),
'gitea' => self::createMokoGiteaAdapter($config),
default => throw new RuntimeException("Unsupported git platform: {$platform}. Use 'github' or 'gitea'."),
'mokogit' => self::createMokoGitAdapter($config),
default => throw new RuntimeException("Unsupported git platform: {$platform}. Use 'github' or 'mokogit'."),
};
}
@@ -87,62 +87,55 @@ class PlatformAdapterFactory
}
/**
* Create a MokoGiteaAdapter with configured ApiClient.
* Create a MokoGitAdapter with configured ApiClient.
*
* @param Config $config Configuration instance
* @return MokoGiteaAdapter Configured Gitea adapter
* @throws RuntimeException If Gitea token is not available
* @return MokoGitAdapter Configured MokoGit adapter
* @throws RuntimeException If MokoGit token is not available
*/
private static function createMokoGiteaAdapter(Config $config): MokoGiteaAdapter
private static function createMokoGitAdapter(Config $config): MokoGitAdapter
{
$token = $config->getString('gitea.token', '');
$token = $config->getString('mokogit.token', '');
if (empty($token)) {
throw new RuntimeException(
'Gitea token not found. Set GA_TOKEN environment variable.'
'MokoGit token not found. Set GA_TOKEN environment variable.'
);
}
$giteaUrl = $config->getString('gitea.url', 'https://git.mokoconsulting.tech');
$apiBaseUrl = rtrim($giteaUrl, '/') . '/api/v1';
$mokogitUrl = $config->getString('mokogit.url', 'https://git.mokoconsulting.tech');
$apiBaseUrl = rtrim($mokogitUrl, '/') . '/api/v1';
$apiClient = new ApiClient(
baseUrl: $apiBaseUrl,
authToken: $token,
maxRequestsPerHour: $config->getInt('gitea.rate_limit', 5000),
maxRetries: $config->getInt('gitea.max_retries', 3),
maxRequestsPerHour: $config->getInt('mokogit.rate_limit', 5000),
maxRetries: $config->getInt('mokogit.max_retries', 3),
authScheme: 'token'
);
return new MokoGiteaAdapter($apiClient, $apiBaseUrl);
return new MokoGitAdapter($apiClient, $apiBaseUrl);
}
/**
* Create adapters for both platforms (useful during migration).
*
* @param Config $config Configuration instance
* @return array{github: GitHubAdapter, gitea: MokoGiteaAdapter} Both adapters
* @return array{github: GitHubAdapter, mokogit: MokoGitAdapter} Both adapters
* @throws RuntimeException If either token is missing
*/
public static function createBoth(Config $config): array
{
return [
'github' => self::createGitHubAdapter($config),
'gitea' => self::createMokoGiteaAdapter($config),
'mokogit' => self::createMokoGitAdapter($config),
];
}
/**
* Sync a file between Gitea (primary) and GitHub (mirror) for a given repo.
* Sync a file between MokoGit (primary) and GitHub (mirror) for a given repo.
*
* Reads the file from Gitea and pushes it to GitHub, ensuring both platforms
* serve identical content. Commonly used for updates.xml sync after releases.
*
* @param Config $config Configuration instance
* @param string $repo Repository name
* @param string $branch Branch to sync (default: 'main')
* @param string $filePath Path to the file (default: 'updates.xml')
* @return bool True if sync succeeded or file was already identical
* @throws RuntimeException If either platform is unreachable
* Reads the file from MokoGit and pushes it to GitHub, ensuring both platforms
* serve identical content.
*/
public static function syncUpdatesBetweenPlatforms(
Config $config,
@@ -151,18 +144,18 @@ class PlatformAdapterFactory
string $filePath = 'updates.xml'
): bool {
$adapters = self::createBoth($config);
$giteaOrg = $config->getString('gitea.organization', 'mokoconsulting-tech');
$mokogitOrg = $config->getString('mokogit.organization', 'mokoconsulting-tech');
$githubOrg = $config->getString('github.organization', 'mokoconsulting-tech');
// Read from Gitea (primary)
// Read from MokoGit (primary)
try {
$giteaFile = $adapters['gitea']->getFileContents($giteaOrg, $repo, $filePath, $branch);
$mokogitFile = $adapters['mokogit']->getFileContents($mokogitOrg, $repo, $filePath, $branch);
} catch (\Exception $e) {
throw new RuntimeException("Failed to read {$filePath} from Gitea ({$giteaOrg}/{$repo}): " . $e->getMessage());
throw new RuntimeException("Failed to read {$filePath} from MokoGit ({$mokogitOrg}/{$repo}): " . $e->getMessage());
}
$giteaContent = base64_decode($giteaFile['content'] ?? '');
if (empty($giteaContent)) {
$mokogitContent = base64_decode($mokogitFile['content'] ?? '');
if (empty($mokogitContent)) {
return false;
}
@@ -173,7 +166,7 @@ class PlatformAdapterFactory
$githubContent = base64_decode($githubFile['content'] ?? '');
$githubSha = $githubFile['sha'] ?? null;
if ($githubContent === $giteaContent) {
if ($githubContent === $mokogitContent) {
return true;
}
} catch (\Exception $e) {
@@ -184,8 +177,8 @@ class PlatformAdapterFactory
$githubOrg,
$repo,
$filePath,
$giteaContent,
"chore(sync): sync {$filePath} from Gitea primary",
$mokogitContent,
"chore(sync): sync {$filePath} from MokoGit primary",
$githubSha,
$branch
);
+98
View File
@@ -0,0 +1,98 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoCli.Enterprise
* INGROUP: MokoCli.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /lib/Enterprise/PlatformDetector.php
* BRIEF: File-based repository platform detection the local-tool replacement for the
* retired .mokogit/manifest.xml <platform> lookup. Remote/API callers should use
* the MokoGit metadata API instead; this class needs no token or network.
*/
declare(strict_types=1);
namespace MokoCli;
/**
* Detects a repository's platform from the files present on disk.
*
* Returns a CANONICAL platform name. Some legacy tools stamp against their own
* vocabulary (e.g. version_set_platform expects 'crm-module' / 'waas-component');
* use toStampPlatform() to translate.
*/
final class PlatformDetector
{
/**
* Detect the canonical platform for a checked-out repository root.
*
* @return string One of: joomla, dolibarr, go, mcp, node, python, platform, generic.
*/
public static function detect(string $root): string
{
// 1. Joomla — pkg_*.xml, Joomla service provider, or template manifest.
$joomla = array_merge(
glob("{$root}/source/pkg_*.xml") ?: [],
glob("{$root}/pkg_*.xml") ?: [],
glob("{$root}/source/packages/*/services/provider.php") ?: [],
glob("{$root}/**/templateDetails.xml") ?: []
);
if (!empty($joomla)) {
return 'joomla';
}
// 2. Dolibarr — module descriptor class + langs dir.
$doli = array_merge(
glob("{$root}/core/modules/mod*.class.php") ?: [],
glob("{$root}/htdocs/**/core/modules/mod*.class.php") ?: [],
glob("{$root}/class/*.class.php") ?: []
);
if (!empty($doli) && is_dir("{$root}/langs")) {
return 'dolibarr';
}
// 3. Go
if (file_exists("{$root}/go.mod")) {
return 'go';
}
// 4. MCP / Node — package.json; MCP if it looks like an MCP server.
if (file_exists("{$root}/package.json")) {
$pkg = json_decode((string) file_get_contents("{$root}/package.json"), true) ?: [];
$name = (string) ($pkg['name'] ?? '');
if (str_contains($name, 'mcp') || isset($pkg['dependencies']['@modelcontextprotocol/sdk'])) {
return 'mcp';
}
return 'node';
}
// 5. Python
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
return 'python';
}
// 6. mokocli itself / org-config repos.
if (in_array(basename($root), ['mokocli', 'MokoCLI'], true)) {
return 'platform';
}
return 'generic';
}
/**
* Translate a canonical platform to the "stamp" vocabulary used by the legacy
* version/release tools (create_repo's TYPE_TO_PLATFORM values).
*/
public static function toStampPlatform(string $canonical): string
{
return match ($canonical) {
'dolibarr' => 'crm-module',
'joomla' => 'waas-component',
default => $canonical,
};
}
}
+3 -3
View File
@@ -288,7 +288,7 @@ class ApiPlugin extends AbstractProjectPlugin
'docker-compose.yml',
'kubernetes/*.yaml',
'tests/ or test/',
'.mokogitea/workflows/* or .gitea/workflows/* or .gitlab-ci.yml',
'.mokogit/workflows/* or .mokogit/workflows/* or .gitlab-ci.yml',
'middleware/ or middlewares/',
];
}
@@ -696,8 +696,8 @@ class ApiPlugin extends AbstractProjectPlugin
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
return $this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, 'Jenkinsfile') ||
$this->fileExists($projectPath, '.circleci');
+5 -5
View File
@@ -80,8 +80,8 @@ class GenericPlugin extends AbstractProjectPlugin
}
// Check for CI/CD configuration
$hasCICD = $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
$hasCICD = $this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, '.travis.yml') ||
$this->fileExists($projectPath, 'Jenkinsfile') ||
@@ -288,7 +288,7 @@ class GenericPlugin extends AbstractProjectPlugin
'CONTRIBUTING.md',
'CODE_OF_CONDUCT.md',
'SECURITY.md',
'.mokogitea/workflows/* or .gitea/workflows/* or .gitlab-ci.yml',
'.mokogit/workflows/* or .mokogit/workflows/* or .gitlab-ci.yml',
'docs/ or documentation/',
'tests/ or test/',
];
@@ -372,8 +372,8 @@ class GenericPlugin extends AbstractProjectPlugin
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
return $this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, '.travis.yml') ||
$this->fileExists($projectPath, 'Jenkinsfile') ||
+3 -3
View File
@@ -344,7 +344,7 @@ class MobilePlugin extends AbstractProjectPlugin
'App icons for all required sizes',
'Splash screen assets',
'tests/ or __tests__/',
'.mokogitea/workflows/* or .gitea/workflows/* or fastlane/',
'.mokogit/workflows/* or .mokogit/workflows/* or fastlane/',
'React Native: metro.config.js',
'Flutter: analysis_options.yaml',
'iOS: Podfile',
@@ -551,8 +551,8 @@ class MobilePlugin extends AbstractProjectPlugin
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
return $this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, 'fastlane') ||
$this->fileExists($projectPath, '.circleci');
+3 -3
View File
@@ -327,7 +327,7 @@ class NodeJsPlugin extends AbstractProjectPlugin
'.nvmrc or .node-version',
'.editorconfig',
'jest.config.js or vitest.config.js',
'.mokogitea/workflows/* or .gitea/workflows/* or .gitlab-ci.yml',
'.mokogit/workflows/* or .mokogit/workflows/* or .gitlab-ci.yml',
];
}
@@ -539,8 +539,8 @@ class NodeJsPlugin extends AbstractProjectPlugin
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
return $this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, '.travis.yml') ||
$this->fileExists($projectPath, '.circleci/config.yml');
+3 -3
View File
@@ -344,7 +344,7 @@ class PythonPlugin extends AbstractProjectPlugin
'pytest.ini or pyproject.toml',
'.python-version or .tool-versions',
'Dockerfile',
'.mokogitea/workflows/* or .gitea/workflows/* or .gitlab-ci.yml',
'.mokogit/workflows/* or .mokogit/workflows/* or .gitlab-ci.yml',
];
}
@@ -566,8 +566,8 @@ class PythonPlugin extends AbstractProjectPlugin
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
return $this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.mokogit/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, '.travis.yml') ||
$this->fileExists($projectPath, 'tox.ini');
+5 -5
View File
@@ -124,15 +124,15 @@ class ProjectMetricsCollector
// Git
$this->collectedMetrics['has_gitignore'] = file_exists("{$path}/.gitignore") ? 1 : 0;
// CI/CD — check both .github/workflows and .gitea/workflows
// CI/CD — check both .github/workflows and .mokogit/workflows
$hasGithubWf = is_dir("{$path}/.github/workflows");
$hasGiteaWf = is_dir("{$path}/.mokogitea/workflows");
$this->collectedMetrics['has_ci_workflows'] = ($hasGithubWf || $hasGiteaWf) ? 1 : 0;
$hasMokoGitWf = is_dir("{$path}/.mokogit/workflows");
$this->collectedMetrics['has_ci_workflows'] = ($hasGithubWf || $hasMokoGitWf) ? 1 : 0;
$this->collectedMetrics['workflow_count'] =
$this->countFiles("{$path}/.github/workflows", '*.yml') +
$this->countFiles("{$path}/.github/workflows", '*.yaml') +
$this->countFiles("{$path}/.mokogitea/workflows", '*.yml') +
$this->countFiles("{$path}/.mokogitea/workflows", '*.yaml');
$this->countFiles("{$path}/.mokogit/workflows", '*.yml') +
$this->countFiles("{$path}/.mokogit/workflows", '*.yaml');
}
private function collectNodeJSMetrics(string $path): void
+9 -9
View File
@@ -204,11 +204,11 @@ class RepositoryHealthChecker
'checks_failed' => 0,
];
// Check both .github/workflows and .gitea/workflows
// Check both .github/workflows and .mokogit/workflows
$githubDir = "{$path}/.github/workflows";
$giteaDir = "{$path}/.mokogitea/workflows";
$hasWorkflowDir = is_dir($githubDir) || is_dir($giteaDir);
$workflowDir = is_dir($giteaDir) ? $giteaDir : $githubDir;
$mokogitDir = "{$path}/.mokogit/workflows";
$hasWorkflowDir = is_dir($githubDir) || is_dir($mokogitDir);
$workflowDir = is_dir($mokogitDir) ? $mokogitDir : $githubDir;
// Check workflows directory exists
$this->addCheck(
@@ -253,15 +253,15 @@ class RepositoryHealthChecker
10
);
// Check for security scanning workflow (CodeQL on GitHub, Trivy on Gitea)
// Check for security scanning workflow (CodeQL on GitHub, Trivy on MokoGit)
$githubWf = "{$path}/.github/workflows";
$giteaWf = "{$path}/.mokogitea/workflows";
$mokogitWf = "{$path}/.mokogit/workflows";
$hasSecurityScan = false;
if (is_dir($githubWf)) {
$hasSecurityScan = !empty(glob("{$githubWf}/*codeql*.yml")) || !empty(glob("{$githubWf}/*codeql*.yaml"));
}
if (!$hasSecurityScan && is_dir($giteaWf)) {
$hasSecurityScan = !empty(glob("{$giteaWf}/*trivy*.yml")) || !empty(glob("{$giteaWf}/*trivy*.yaml"));
if (!$hasSecurityScan && is_dir($mokogitWf)) {
$hasSecurityScan = !empty(glob("{$mokogitWf}/*trivy*.yml")) || !empty(glob("{$mokogitWf}/*trivy*.yaml"));
}
$this->addCheck(
$category,
@@ -270,7 +270,7 @@ class RepositoryHealthChecker
10
);
// Check for dependency management (Dependabot on GitHub, Renovate on Gitea)
// Check for dependency management (Dependabot on GitHub, Renovate on MokoGit)
$this->addCheck(
$category,
'Dependency management configured',
+12 -12
View File
@@ -62,7 +62,7 @@ class RepositorySynchronizer
?CheckpointManager $checkpoints = null,
?GitPlatformAdapter $adapter = null
) {
$this->adapter = $adapter ?? new MokoGiteaAdapter($apiClient);
$this->adapter = $adapter ?? new MokoGitAdapter($apiClient);
$this->logger = $logger;
$this->metrics = $metrics;
$this->checkpoints = $checkpoints ?? new CheckpointManager('.checkpoints');
@@ -266,7 +266,7 @@ class RepositorySynchronizer
/**
* Read the platform slug from the remote manifest.xml.
* Checks .mokogitea/manifest.xml, then legacy locations (.mokostandards, .github/.mokostandards).
* Checks .mokogit/manifest.xml, then legacy locations (.mokostandards, .github/.mokostandards).
*
* @return string|null Platform slug or null if not found/parseable
*/
@@ -278,7 +278,7 @@ class RepositorySynchronizer
"{$metaDir}/.mokostandards",
'.mokostandards',
];
if ($metaDir === '.mokogitea') {
if ($metaDir === '.mokogit') {
$paths[] = '.github/.mokostandards';
}
@@ -633,11 +633,11 @@ class RepositorySynchronizer
}
/**
* Migrate legacy .mokostandards to the platform metadata dir (.gitea/ or .github/)
* Migrate legacy .mokostandards to the platform metadata dir (.mokogit/ or .github/)
* and convert legacy YAML-like format to the new XML manifest.
*
* Handles:
* 1. Location migration: root or .github/ .mokogitea/manifest.xml
* 1. Location migration: root or .github/ .mokogit/manifest.xml
* 2. Format migration: legacy "platform: xxx" XML manifest
* 3. Update existing XML: refresh <governance><last-synced> timestamp
*/
@@ -654,7 +654,7 @@ class RepositorySynchronizer
// ── Collect existing files from all legacy locations ─────────
$legacySources = ['.mokostandards'];
if ($metaDir === '.mokogitea') {
if ($metaDir === '.mokogit') {
$legacySources[] = '.github/.mokostandards';
}
@@ -762,7 +762,7 @@ class RepositorySynchronizer
* @param string $org Organization name
* @param string $repo Repository name
* @param string $platform Detected platform slug
* @param array $repoInfo Gitea API repo object
* @param array $repoInfo MokoGit API repo object
* @param string|null $existingContent Current .mokostandards content (XML or legacy)
* @return string Well-formed XML content
*/
@@ -1069,10 +1069,10 @@ class RepositorySynchronizer
}
// Read all .yml files from the template's workflow directory
$sourceDir = "{$cacheDir}/.mokogitea/workflows";
// Fallback to legacy path if .mokogitea doesn't exist
$sourceDir = "{$cacheDir}/.mokogit/workflows";
// Fallback to legacy path if .mokogit doesn't exist
if (!is_dir($sourceDir)) {
$sourceDir = "{$cacheDir}/.gitea/workflows";
$sourceDir = "{$cacheDir}/.mokogit/workflows";
}
$shared = [];
@@ -1083,9 +1083,9 @@ class RepositorySynchronizer
}
}
// CODEOWNERS — GitHub only; Gitea doesn't enforce it
// CODEOWNERS — GitHub only; MokoGit doesn't enforce it
if ($this->adapter->getPlatformName() === 'github') {
$shared[] = ['templates/mokogitea/CODEOWNERS', '.github/CODEOWNERS'];
$shared[] = ['templates/mokogit/CODEOWNERS', '.github/CODEOWNERS'];
}
// Platform-specific gitignore (merged, not replaced)
+5 -5
View File
@@ -58,7 +58,7 @@ class SourceResolver
* Resolve the source directory name for a repository root.
*
* Resolution order:
* 1. Gitea Manifest API `entry_point` (when GA_TOKEN/GITEA_TOKEN + GITHUB_REPOSITORY are set)
* 1. MokoGit Manifest API `entry_point` (when GA_TOKEN/MOKOGIT_TOKEN + GITHUB_REPOSITORY are set)
* 2. First candidate directory that exists on the filesystem
* 3. 'source' as the default (e.g. for new repos being scaffolded)
*
@@ -84,9 +84,9 @@ class SourceResolver
}
/**
* Query the MokoGitea Manifest API for the entry_point field.
* Query the MokoGit Manifest API for the entry_point field.
*
* Only attempts the call when GA_TOKEN or GITEA_TOKEN is set. Results are
* Only attempts the call when GA_TOKEN or MOKOGIT_TOKEN is set. Results are
* cached per org/repo for the lifetime of the process.
*
* @param string $root Repository root (used to derive org/repo from git remote).
@@ -94,7 +94,7 @@ class SourceResolver
*/
public static function resolveFromApi(string $root): ?string
{
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
$token = getenv('GA_TOKEN') ?: getenv('MOKOGIT_TOKEN') ?: '';
if ($token === '') {
return null;
}
@@ -109,7 +109,7 @@ class SourceResolver
return self::$apiCache[$cacheKey];
}
$baseUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$baseUrl = rtrim(getenv('MOKOGIT_URL') ?: 'https://git.mokoconsulting.tech', '/');
$url = "{$baseUrl}/api/v1/repos/{$org}/{$repo}/manifest";
$ctx = stream_context_create([

Some files were not shown because too many files have changed in this diff Show More