Public Access
+37
-896
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
#
|
||||
# +========================================================================+
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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}]}" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 === '') {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}"]);
|
||||
|
||||
@@ -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());
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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') ||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user