Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bd26698c4 | |||
| 19b504526b | |||
| e7bdf7cbc7 | |||
| ff5794d0cc | |||
| bfba45e8b5 | |||
| 78ea05233b | |||
| ae0d54310d | |||
| 9df59836bf | |||
| 6e40707223 | |||
| ca55e5d2d2 | |||
| 9526d006c4 | |||
| c90a5671bd | |||
| 048a7d71d1 | |||
| c57b5724ac | |||
| 78affd37ff | |||
| b3062c6559 | |||
| 9dab9f1ef6 | |||
| c61d32709c | |||
| 2b137f9041 | |||
| 54a27c0a8f | |||
| 5754fae5a8 | |||
| ab3c0a3a8d | |||
| eb3689cff6 | |||
| 7338a3da2e | |||
| 0a0e1f11e0 | |||
| c3a3ab3f62 | |||
| 556ac85a63 | |||
| c1a145480c | |||
| ab7b6cfba1 | |||
| 2d6155d655 | |||
| 65215cdc4c | |||
| 8c87cf1e74 | |||
| 59d3524615 | |||
| 8058baef95 | |||
| df2efa4838 | |||
| 76bc91a383 | |||
| b53846f6f4 |
@@ -0,0 +1,76 @@
|
||||
# moko-platform
|
||||
|
||||
Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Language** | PHP 8.1+ |
|
||||
| **Version** | 09.01.00 |
|
||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||
| **Wiki** | [moko-platform Wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) |
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
composer install # Install PHP dependencies
|
||||
php bin/moko health --path . # Repo health check
|
||||
php bin/moko check:syntax --path . # PHP syntax check
|
||||
php bin/moko drift --org MokoConsulting # Scan for standards drift
|
||||
php bin/moko dashboard --token $TOKEN -o dashboard.html # Client dashboard
|
||||
|
||||
# Code quality
|
||||
php vendor/bin/phpcs --standard=phpcs.xml -n lib/ validate/ automation/ cli/
|
||||
php vendor/bin/phpcbf --standard=phpcs.xml lib/ validate/ automation/ cli/
|
||||
php vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M
|
||||
composer check # Run all checks
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
| Directory | Purpose |
|
||||
|---|---|
|
||||
| `cli/` | 32 standalone CLI tools (version, release, build, repo management) |
|
||||
| `validate/` | 20 validation scripts (syntax, structure, manifests, drift) |
|
||||
| `automation/` | 7 bulk operations (sync, push files, templates, cleanup) |
|
||||
| `lib/Enterprise/` | Core library — CliFramework, ApiClient, adapters, validators |
|
||||
| `lib/Enterprise/Plugins/` | 11 platform plugins (Joomla, Dolibarr, Node.js, Python, etc.) |
|
||||
| `deploy/` | SFTP deployment scripts (Joomla, Dolibarr, health checks) |
|
||||
| `templates/` | Universal templates, configs, governance schema |
|
||||
| `.mokogitea/workflows/` | CI/CD workflows (Gitea Actions) |
|
||||
| `bin/moko` | Unified CLI dispatcher — `php bin/moko <command>` |
|
||||
| `monitoring/sites.json` | Sites list for mcp_mokomonitor |
|
||||
|
||||
### CLI Framework
|
||||
|
||||
All CLI tools extend `MokoEnterprise\CliFramework` (`lib/Enterprise/CliFramework.php`).
|
||||
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`.
|
||||
After adding a CLI tool, register it in `bin/moko` COMMAND_MAP.
|
||||
|
||||
### Platform Adapters
|
||||
|
||||
- `MokoGiteaAdapter` — git.mokoconsulting.tech (primary)
|
||||
- `GitHubAdapter` — github.com mirrors
|
||||
|
||||
### Plugin System
|
||||
|
||||
Platform-specific logic in `lib/Enterprise/Plugins/`. Each implements `ProjectPluginInterface` with health checks, validation, build commands, config schemas.
|
||||
|
||||
## Code Quality
|
||||
|
||||
| Tool | Level | Config |
|
||||
|---|---|---|
|
||||
| PHPCS | PSR-12 (errors only) | `phpcs.xml` |
|
||||
| PHPStan | Level 2 (advisory) | `phpstan.neon` |
|
||||
|
||||
PHPStan runs with `--memory-limit=512M`. CI enforces PHPCS errors; PHPStan is `continue-on-error`.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||
- **Attribution**: `Authored-by: Moko Consulting`
|
||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
|
||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||
@@ -1,66 +1,68 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if [ -f "/opt/moko-platform/cli/version_bump.php" ] && [ -f "/opt/moko-platform/vendor/autoload.php" ]; then
|
||||
echo "Using pre-installed /opt/moko-platform"
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||
# | joomla: XML manifest, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
@@ -71,20 +71,25 @@ jobs:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
@@ -100,16 +105,15 @@ jobs:
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
@@ -151,25 +155,60 @@ jobs:
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
# Ensure PHP + Composer are available
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||
else
|
||||
NOTES="Stable release"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
@@ -182,7 +221,7 @@ jobs:
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
@@ -256,7 +295,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/ci-platform.yml
|
||||
# PATH: /.mokogitea/workflows/ci-platform.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: moko-platform CI — the standards engine validates itself
|
||||
#
|
||||
@@ -41,7 +41,7 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'wiki/**'
|
||||
- '.gitea/ISSUE_TEMPLATE/**'
|
||||
- '.mokogitea/ISSUE_TEMPLATE/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
echo "::error file=${file}::PHP syntax error"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done < <(find lib/ validate/ automation/ cli/ src/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||
done < <(find lib/ validate/ automation/ cli/ source/ src/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||
|
||||
{
|
||||
echo "### PHP Syntax"
|
||||
@@ -270,7 +270,7 @@ jobs:
|
||||
echo "::warning file=${file}::Missing SPDX header"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||
done < <(find lib/ validate/ cli/ source/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||
|
||||
{
|
||||
echo "### License Headers"
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
echo "::error file=${file}::Potential hardcoded secret detected"
|
||||
FOUND=$((FOUND + 1))
|
||||
fi
|
||||
done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||
done < <(find lib/ validate/ cli/ source/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||
|
||||
{
|
||||
echo "### Secret Detection"
|
||||
@@ -412,6 +412,12 @@ jobs:
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: automation/ci-issue-reporter.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Check gate results
|
||||
run: |
|
||||
{
|
||||
@@ -437,3 +443,46 @@ jobs:
|
||||
echo "::error::One or more CI gates failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: "File issues for failed gates"
|
||||
if: >-
|
||||
always() &&
|
||||
(needs.code-quality.result == 'failure' ||
|
||||
needs.tests.result == 'failure' ||
|
||||
needs.self-health.result == 'failure' ||
|
||||
needs.governance.result == 'failure' ||
|
||||
needs.templates.result == 'failure')
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
chmod +x automation/ci-issue-reporter.sh
|
||||
REPORTER="./automation/ci-issue-reporter.sh"
|
||||
WF="Platform CI"
|
||||
|
||||
report_gate() {
|
||||
local gate="$1" result="$2" details="$3"
|
||||
if [ "$result" = "failure" ]; then
|
||||
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||
fi
|
||||
}
|
||||
|
||||
report_gate "Code Quality" \
|
||||
"${{ needs.code-quality.result }}" \
|
||||
"PHPCS (PSR-12), PHPStan, or PHP syntax checks failed. Run \`composer check\` locally to reproduce."
|
||||
|
||||
report_gate "Unit Tests" \
|
||||
"${{ needs.tests.result }}" \
|
||||
"PHPUnit tests failed on one or more PHP versions (8.1, 8.2, 8.3). Run \`vendor/bin/phpunit --testdox\` locally."
|
||||
|
||||
report_gate "Self-Health" \
|
||||
"${{ needs.self-health.result }}" \
|
||||
"Self-health score fell below the 80% threshold. Run \`php bin/moko health -- --path .\` locally."
|
||||
|
||||
report_gate "Governance" \
|
||||
"${{ needs.governance.result }}" \
|
||||
"Governance checks failed (license headers, secrets, or version consistency). Check the CI run summary for specifics."
|
||||
|
||||
report_gate "Template Integrity" \
|
||||
"${{ needs.templates.result }}" \
|
||||
"Workflow or gitignore templates failed YAML validation or are missing required entries."
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/cleanup.yml
|
||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 09.24.00
|
||||
# VERSION: 09.25.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# PATH: /.mokogitea/workflows/notify.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
|
||||
@@ -172,7 +172,8 @@ jobs:
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
MISSING=0
|
||||
SOURCE_DIR="src"
|
||||
SOURCE_DIR="source"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||
while IFS= read -r dir; do
|
||||
if [ ! -f "${dir}/index.html" ]; then
|
||||
@@ -220,7 +221,7 @@ jobs:
|
||||
echo "joomla.asset.json: valid"
|
||||
fi
|
||||
|
||||
# Validate all XML files in src/ are well-formed
|
||||
# Validate all XML files in source/src/ are well-formed
|
||||
XML_ERRORS=0
|
||||
if command -v php &> /dev/null; then
|
||||
while IFS= read -r -d '' xmlfile; do
|
||||
@@ -451,10 +452,11 @@ jobs:
|
||||
|
||||
- name: Verify package source
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
SOURCE_DIR="source"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "::warning::No src/ or htdocs/ directory"
|
||||
echo "::warning::No source/, src/, or htdocs/ directory"
|
||||
exit 0
|
||||
fi
|
||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 09.23.00
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
@@ -17,6 +17,10 @@ on:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
@@ -43,7 +47,8 @@ jobs:
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -51,22 +56,28 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
@@ -76,31 +87,43 @@ jobs:
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) TAG="development" ;;
|
||||
alpha) TAG="alpha" ;;
|
||||
beta) TAG="beta" ;;
|
||||
release-candidate) TAG="release-candidate" ;;
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Bump version: patch for dev/alpha/beta, minor for RC
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate) php ${MOKO_CLI}/version_bump.php --path . --minor 2>/dev/null || true ;;
|
||||
*) php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true ;;
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
# Set stability suffix and fix consistency
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Read final version with suffix
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Append suffix for output
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
@@ -125,11 +148,12 @@ jobs:
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
@@ -142,6 +166,41 @@ jobs:
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
else
|
||||
NOTES="Release ${VERSION}"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
|
||||
@@ -296,17 +296,19 @@ jobs:
|
||||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos)
|
||||
SOURCE_DIR=""
|
||||
if [ -d "src" ]; then
|
||||
if [ -d "source" ]; then
|
||||
SOURCE_DIR="source"
|
||||
elif [ -d "src" ]; then
|
||||
SOURCE_DIR="src"
|
||||
elif [ -d "htdocs" ]; then
|
||||
SOURCE_DIR="htdocs"
|
||||
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||
# Platform/tooling repos don't need src/
|
||||
# Platform/tooling repos don't need source/
|
||||
SOURCE_DIR=""
|
||||
else
|
||||
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||
missing_required+=("source/ or src/ or htdocs/ (source directory required)")
|
||||
fi
|
||||
|
||||
for item in "${required_artifacts[@]}"; do
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Security
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/security-audit.yml
|
||||
# PATH: /.mokogitea/workflows/security-audit.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/update-server.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||
#
|
||||
# Thin wrapper around moko-platform CLI tools.
|
||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||
#
|
||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||
|
||||
name: "Update Server"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Stability tag'
|
||||
required: true
|
||||
default: 'development'
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- stable
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-xml:
|
||||
name: Update Server
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform 2>/dev/null || true
|
||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve stability and bump version
|
||||
id: meta
|
||||
run: |
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
|
||||
# Configure git for bot pushes
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
# Determine stability from branch or manual input
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
STABILITY="${{ inputs.stability }}"
|
||||
elif [[ "$BRANCH" == rc/* ]]; then
|
||||
STABILITY="rc"
|
||||
elif [[ "$BRANCH" == beta/* ]]; then
|
||||
STABILITY="beta"
|
||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||
STABILITY="alpha"
|
||||
else
|
||||
STABILITY="development"
|
||||
fi
|
||||
|
||||
# Gitea release tag per stability
|
||||
case "$STABILITY" in
|
||||
development) TAG="development" ;;
|
||||
alpha) TAG="alpha" ;;
|
||||
beta) TAG="beta" ;;
|
||||
rc) TAG="release-candidate" ;;
|
||||
*) TAG="stable" ;;
|
||||
esac
|
||||
|
||||
# Bump patch, set platform suffix, fix consistency — version_bump preserves suffix
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
|
||||
--branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Read final version (includes suffix, e.g. 01.02.15-dev)
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Commit version bump if changed
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||
git push
|
||||
}
|
||||
|
||||
- name: Create release and upload package
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Create or update Gitea release
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
# Build package and upload
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
- name: Update updates.xml
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
|
||||
if [ ! -f "updates.xml" ]; then
|
||||
echo "No updates.xml — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHA_FLAG=""
|
||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||
|
||||
php ${MOKO_CLI}/updates_xml_build.php \
|
||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
${SHA_FLAG}
|
||||
|
||||
# Commit and push updates.xml
|
||||
git add updates.xml
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||
git push
|
||||
}
|
||||
|
||||
- name: Sync updates.xml to main
|
||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||
python3 -c "
|
||||
import base64, json, urllib.request, sys
|
||||
with open('updates.xml', 'rb') as f:
|
||||
content = base64.b64encode(f.read()).decode()
|
||||
payload = json.dumps({
|
||||
'content': content,
|
||||
'sha': '${FILE_SHA}',
|
||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||
'branch': 'main'
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/contents/updates.xml',
|
||||
data=payload, method='PUT',
|
||||
headers={
|
||||
'Authorization': 'token ${GITEA_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
try:
|
||||
urllib.request.urlopen(req)
|
||||
print('updates.xml synced to main')
|
||||
except Exception as e:
|
||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||
"
|
||||
fi
|
||||
|
||||
- name: SFTP deploy to dev server
|
||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
run: |
|
||||
# Permission check: admin or maintain role required
|
||||
ACTOR="${{ github.actor }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||
case "$PERMISSION" in
|
||||
admin|maintain|write) ;;
|
||||
*)
|
||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||
|
||||
PORT="${DEV_PORT:-22}"
|
||||
REMOTE="${DEV_PATH%/}"
|
||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||
if [ -n "$DEV_KEY" ]; then
|
||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
fi
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
DISPLAY="${VERSION}"
|
||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
+1
-1
@@ -13,7 +13,7 @@ BRIEF: Release changelog
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [09.24.00] --- 2026-06-04
|
||||
## [09.25.00] --- 2026-06-04
|
||||
|
||||
## [09.23] --- 2026-05-31
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code when working with this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**moko-platform** — Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Language** | PHP 8.1+ |
|
||||
| **Default branch** | main |
|
||||
| **License** | GPL-3.0-or-later |
|
||||
| **Version** | 09.01.00 |
|
||||
| **Wiki** | [moko-platform Wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) |
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
composer install # Install PHP dependencies
|
||||
php bin/moko health --path . # Run repo health check
|
||||
php bin/moko check:syntax --path . # PHP syntax check
|
||||
php bin/moko drift --org MokoConsulting # Scan for standards drift
|
||||
php bin/moko dashboard --token $TOKEN -o dashboard.html # Generate client dashboard
|
||||
|
||||
# Code quality
|
||||
php vendor/bin/phpcs --standard=phpcs.xml -n lib/ validate/ automation/ cli/
|
||||
php vendor/bin/phpcbf --standard=phpcs.xml lib/ validate/ automation/ cli/
|
||||
php vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M
|
||||
|
||||
# Run all checks
|
||||
composer check
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Layout
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `cli/` | 32 standalone CLI tools (version, release, build, repo management) |
|
||||
| `validate/` | 20 validation scripts (syntax, structure, manifests, drift) |
|
||||
| `automation/` | 7 bulk operations (sync, push files, templates, cleanup) |
|
||||
| `lib/Enterprise/` | Core library — CliFramework, ApiClient, adapters, validators |
|
||||
| `lib/Enterprise/Plugins/` | 11 platform plugins (Joomla, Dolibarr, Node.js, Python, etc.) |
|
||||
| `deploy/` | SFTP deployment scripts (Joomla, Dolibarr, health checks) |
|
||||
| `templates/` | Universal templates, configs, governance schema |
|
||||
| `.mokogitea/workflows/` | CI/CD workflows (Gitea Actions) |
|
||||
| `bin/moko` | Unified CLI dispatcher — runs any tool via `php bin/moko <command>` |
|
||||
|
||||
### CLI Framework
|
||||
|
||||
All CLI tools extend `MokoEnterprise\CliFramework` (defined in `lib/Enterprise/CliFramework.php`).
|
||||
|
||||
Pattern for new tools:
|
||||
```php
|
||||
class MyTool extends CliFramework {
|
||||
protected function configure(): void {
|
||||
$this->setDescription('What this tool does');
|
||||
$this->addArgument('--name', 'Description', 'default');
|
||||
}
|
||||
protected function run(): int {
|
||||
$name = $this->getArgument('--name');
|
||||
// ... business logic ...
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
$app = new MyTool();
|
||||
exit($app->execute());
|
||||
```
|
||||
|
||||
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`
|
||||
|
||||
### Platform Adapters
|
||||
|
||||
Git operations are abstracted via `GitPlatformAdapter` interface:
|
||||
- `MokoGiteaAdapter` — for git.mokoconsulting.tech (primary)
|
||||
- `GitHubAdapter` — for github.com mirrors
|
||||
|
||||
### Plugin System
|
||||
|
||||
Platform-specific logic lives in `lib/Enterprise/Plugins/`. Each plugin implements `ProjectPluginInterface` with methods for health checks, validation, build commands, and config schemas.
|
||||
|
||||
## Code Quality
|
||||
|
||||
| Tool | Level | Config |
|
||||
|------|-------|--------|
|
||||
| PHPCS | PSR-12 (errors only) | `phpcs.xml` |
|
||||
| PHPStan | Level 2 | `phpstan.neon` |
|
||||
|
||||
PHPStan runs with `--memory-limit=512M` due to large codebase. CI enforces PHPCS errors; PHPStan is advisory (`continue-on-error`).
|
||||
|
||||
## Rules
|
||||
|
||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
||||
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
||||
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
|
||||
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
||||
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
|
||||
- **After adding a CLI tool**: register it in `bin/moko` COMMAND_MAP
|
||||
@@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root
|
||||
INGROUP: MokoPlatform
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
PATH: /README.md
|
||||
VERSION: 09.24.00
|
||||
VERSION: 09.25.00
|
||||
BRIEF: Project overview and documentation
|
||||
-->
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Automation.CI
|
||||
# INGROUP: moko-platform.Automation
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /automation/ci-issue-reporter.sh
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Creates or updates a Gitea 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.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||
REPO="${GITHUB_REPOSITORY:-}"
|
||||
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||
LABEL_NAME="ci-auto"
|
||||
LABEL_COLOR="#e11d48"
|
||||
|
||||
GATE=""
|
||||
DETAILS=""
|
||||
SEVERITY="error"
|
||||
WORKFLOW=""
|
||||
|
||||
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||
|
||||
Required:
|
||||
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||
--details Human-readable failure description
|
||||
|
||||
Optional:
|
||||
--severity "error" (default) or "warning"
|
||||
--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)
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--gate) GATE="$2"; shift 2 ;;
|
||||
--details) DETAILS="$2"; shift 2 ;;
|
||||
--severity) SEVERITY="$2"; shift 2 ;;
|
||||
--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 ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
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 "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||
|
||||
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||
|
||||
# ── Build title ─────────────────────────────────────────────────────────────
|
||||
if [[ -n "$WORKFLOW" ]]; then
|
||||
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||
else
|
||||
TITLE="[CI] ${GATE} failed"
|
||||
fi
|
||||
|
||||
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||
ensure_label() {
|
||||
local exists
|
||||
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||
-H "Authorization: token ${GITEA_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}" \
|
||||
"${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 "Content-Type: application/json" \
|
||||
"${API}/labels" \
|
||||
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||
> /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Search for existing open issue ──────────────────────────────────────────
|
||||
find_existing_issue() {
|
||||
# URL-encode the gate name for the query
|
||||
local query
|
||||
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||
|
||||
local response
|
||||
response=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||
2>/dev/null || echo "[]")
|
||||
|
||||
# Extract the first matching issue number
|
||||
echo "$response" \
|
||||
| grep -oP '"number":\s*\K[0-9]+' \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# ── Build issue body ────────────────────────────────────────────────────────
|
||||
build_body() {
|
||||
local severity_badge
|
||||
if [[ "$SEVERITY" == "error" ]]; then
|
||||
severity_badge="**Severity:** Error"
|
||||
else
|
||||
severity_badge="**Severity:** Warning"
|
||||
fi
|
||||
|
||||
cat <<BODY
|
||||
## CI Gate Failure: ${GATE}
|
||||
|
||||
${severity_badge}
|
||||
**Workflow:** ${WORKFLOW:-unknown}
|
||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||
**Run:** [View CI run](${RUN_URL})
|
||||
|
||||
### Details
|
||||
|
||||
${DETAILS}
|
||||
|
||||
### Resolution
|
||||
|
||||
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)*
|
||||
BODY
|
||||
}
|
||||
|
||||
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||
build_comment() {
|
||||
cat <<COMMENT
|
||||
### CI failure recurrence
|
||||
|
||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||
**Run:** [View CI run](${RUN_URL})
|
||||
|
||||
${DETAILS}
|
||||
COMMENT
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
ensure_label
|
||||
|
||||
EXISTING=$(find_existing_issue)
|
||||
|
||||
if [[ -n "$EXISTING" ]]; then
|
||||
# Append comment to existing issue
|
||||
COMMENT_BODY=$(build_comment)
|
||||
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||
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 "Content-Type: application/json" \
|
||||
"${API}/issues/${EXISTING}/comments" \
|
||||
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||
|
||||
if [[ "$HTTP" == "201" ]]; then
|
||||
echo "Commented on existing issue #${EXISTING}"
|
||||
else
|
||||
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||
fi
|
||||
else
|
||||
# Create new issue
|
||||
ISSUE_BODY=$(build_body)
|
||||
ISSUE_JSON=$(python3 -c "
|
||||
import sys, json
|
||||
body = sys.stdin.read()
|
||||
print(json.dumps({
|
||||
'title': sys.argv[1],
|
||||
'body': body,
|
||||
'labels': []
|
||||
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||
|
||||
# Create the issue
|
||||
RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues" \
|
||||
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||
|
||||
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)
|
||||
LABEL_ID=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_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 "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||
> /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||
else
|
||||
echo "WARNING: Failed to create issue"
|
||||
echo "Response: ${RESPONSE}"
|
||||
fi
|
||||
fi
|
||||
@@ -230,7 +230,8 @@ class PushFiles extends CliFramework
|
||||
{
|
||||
// Read platform from repo's .mokogitea/manifest.xml via API
|
||||
try {
|
||||
$manifestData = $this->adapter->getFileContent($org, $repo, '.mokogitea/manifest.xml', 'main');
|
||||
$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) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/branch_rename.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/bulk_workflow_push.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/bulk_workflow_trigger.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Trigger a workflow across multiple repos at once
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/client_dashboard.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Generate unified client dashboard HTML
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/client_inventory.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/client_provision.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Provision a new client environment end-to-end
|
||||
*/
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
@@ -866,11 +866,11 @@ class DeployJoomla extends CliFramework
|
||||
}
|
||||
}
|
||||
|
||||
// 3-5. Fallback chain
|
||||
foreach (['src', 'htdocs'] as $candidate) {
|
||||
if (is_dir("{$repoPath}/{$candidate}")) {
|
||||
return "{$repoPath}/{$candidate}";
|
||||
}
|
||||
// 3-5. Fallback chain (source/ → src/ → htdocs/)
|
||||
$resolved = SourceResolver::resolveAbsolute($repoPath);
|
||||
if ($resolved !== null) {
|
||||
SourceResolver::warnIfLegacy($repoPath);
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
// Last resort: repo root itself
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/grafana_dashboard.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Manage Grafana dashboards via API
|
||||
*/
|
||||
|
||||
|
||||
+5
-10
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/joomla_build.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
||||
* NOTE: Called by pre-release and auto-release workflows.
|
||||
*/
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class JoomlaBuildCli extends CliFramework
|
||||
{
|
||||
@@ -49,17 +49,12 @@ class JoomlaBuildCli extends CliFramework
|
||||
$path = realpath($path) ?: $path;
|
||||
|
||||
// ── Find source directory ──────────────────────────────────────────────
|
||||
$srcDir = null;
|
||||
foreach (['src', 'htdocs'] as $d) {
|
||||
if (is_dir("{$path}/{$d}")) {
|
||||
$srcDir = "{$path}/{$d}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||
if ($srcDir === null) {
|
||||
$this->log('ERROR', "::error::No src/ or htdocs/ directory in {$path}");
|
||||
$this->log('ERROR', "::error::No source/ or src/ directory in {$path}");
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
|
||||
// ── Find manifest ──────────────────────────────────────────────────────
|
||||
$manifest = $this->findManifest($srcDir);
|
||||
|
||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
|
||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
|
||||
|
||||
/**
|
||||
* Joomla Release Manager
|
||||
@@ -121,11 +121,12 @@ class JoomlaRelease extends CliFramework
|
||||
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
|
||||
|
||||
// ── Step 3: Build packages ────────────────────────────────────
|
||||
$srcDir = is_dir("{$path}/src") ? "{$path}/src" : (is_dir("{$path}/htdocs") ? "{$path}/htdocs" : null);
|
||||
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||
if ($srcDir === null) {
|
||||
$this->log('ERROR', 'No src/ or htdocs/ directory');
|
||||
$this->log('ERROR', 'No source/ or src/ directory');
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
|
||||
$prefix = $this->typePrefix($meta);
|
||||
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ManifestElementCli extends CliFramework
|
||||
{
|
||||
@@ -48,7 +48,7 @@ class ManifestElementCli extends CliFramework
|
||||
}
|
||||
}
|
||||
$extManifest = null;
|
||||
$manifestFiles = array_merge(glob("{$root}/src/pkg_*.xml") ?: [], glob("{$root}/src/*.xml") ?: [], glob("{$root}/*.xml") ?: []);
|
||||
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []);
|
||||
foreach ($manifestFiles as $file) {
|
||||
$c = file_get_contents($file);
|
||||
if (strpos($c, '<extension') !== false) {
|
||||
@@ -58,8 +58,7 @@ class ManifestElementCli extends CliFramework
|
||||
}
|
||||
$modFile = null;
|
||||
$modFiles = array_merge(
|
||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||
);
|
||||
foreach ($modFiles as $file) {
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: moko-platform.CLI
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/manifest_licensing.php
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
/**
|
||||
* Reads the <licensing> block from .mokogitea/manifest.xml and ensures that the
|
||||
* Joomla extension manifest contains the correct <updateservers> and <dlid> tags.
|
||||
*
|
||||
* manifest.xml licensing block example:
|
||||
*
|
||||
* <licensing>
|
||||
* <enabled>true</enabled>
|
||||
* <dlid>true</dlid>
|
||||
* <update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
|
||||
* <update-server-name>MyExtension Updates</update-server-name>
|
||||
* </licensing>
|
||||
*
|
||||
* Supports {org} and {repo} placeholders in update-server URL, resolved from
|
||||
* the manifest's <identity> block or git remote.
|
||||
*/
|
||||
class ManifestLicensingCli extends CliFramework
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests');
|
||||
$this->addArgument('--path', 'Repository root path', '.');
|
||||
$this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false);
|
||||
$this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false);
|
||||
}
|
||||
|
||||
protected function run(): int
|
||||
{
|
||||
$root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
|
||||
$fix = (bool) $this->getArgument('--fix');
|
||||
$ghOutput = (bool) $this->getArgument('--github-output');
|
||||
|
||||
// ── 1. Read manifest.xml ──────────────────────────────────────────
|
||||
$manifestFile = "{$root}/.mokogitea/manifest.xml";
|
||||
|
||||
if (!file_exists($manifestFile)) {
|
||||
$this->log('WARN', "No manifest.xml found at {$manifestFile}");
|
||||
$this->outputResult($ghOutput, 'skipped', 'No manifest.xml');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$xml = @simplexml_load_file($manifestFile);
|
||||
|
||||
if ($xml === false) {
|
||||
$this->log('ERROR', "Failed to parse {$manifestFile}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ── 2. Check if licensing is enabled ──────────────────────────────
|
||||
if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') {
|
||||
$this->log('INFO', 'Licensing not enabled in manifest.xml — skipping');
|
||||
$this->outputResult($ghOutput, 'skipped', 'Licensing not enabled');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$licensingNode = $xml->licensing;
|
||||
$dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true';
|
||||
$updateServerUrl = (string) ($licensingNode->{'update-server'} ?? '');
|
||||
$updateServerName = (string) ($licensingNode->{'update-server-name'} ?? '');
|
||||
|
||||
// ── 3. Resolve placeholders ───────────────────────────────────────
|
||||
$org = (string) ($xml->identity->org ?? '');
|
||||
$repo = (string) ($xml->identity->name ?? '');
|
||||
|
||||
// Fallback to git remote if manifest doesn't have org/name
|
||||
if (empty($org) || empty($repo)) {
|
||||
$remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null"));
|
||||
|
||||
if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) {
|
||||
if (empty($org)) {
|
||||
$org = $m[1];
|
||||
}
|
||||
if (empty($repo)) {
|
||||
$repo = $m[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default update server URL if not specified
|
||||
if (empty($updateServerUrl) && !empty($org) && !empty($repo)) {
|
||||
$updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml";
|
||||
}
|
||||
|
||||
// Resolve {org} and {repo} placeholders
|
||||
$updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl);
|
||||
|
||||
// Default server name from display-name or repo name
|
||||
if (empty($updateServerName)) {
|
||||
$displayName = (string) ($xml->identity->{'display-name'} ?? $repo);
|
||||
$updateServerName = $displayName . ' Updates';
|
||||
}
|
||||
|
||||
if (empty($updateServerUrl)) {
|
||||
$this->log('ERROR', 'Cannot determine update server URL — set <update-server> in manifest.xml or ensure org/repo are available');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}");
|
||||
$this->log('INFO', "Update server: {$updateServerUrl}");
|
||||
$this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no'));
|
||||
|
||||
// ── 4. Find Joomla extension manifests ────────────────────────────
|
||||
$xmlFiles = array_merge(
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
|
||||
$packageManifest = null;
|
||||
|
||||
foreach ($xmlFiles as $file) {
|
||||
$content = file_get_contents($file);
|
||||
|
||||
if (!str_contains($content, '<extension')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the package manifest (type="package") or the main extension manifest
|
||||
if (str_contains($content, 'type="package"')) {
|
||||
$packageManifest = $file;
|
||||
break;
|
||||
}
|
||||
|
||||
// Fallback: first extension manifest found
|
||||
if ($packageManifest === null) {
|
||||
$packageManifest = $file;
|
||||
}
|
||||
}
|
||||
|
||||
if ($packageManifest === null) {
|
||||
$this->log('WARN', 'No Joomla extension manifest found');
|
||||
$this->outputResult($ghOutput, 'skipped', 'No extension manifest');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest));
|
||||
$this->log('INFO', "Package manifest: {$relPath}");
|
||||
|
||||
// ── 5. Check and fix the manifest ─────────────────────────────────
|
||||
$content = file_get_contents($packageManifest);
|
||||
$original = $content;
|
||||
$changes = [];
|
||||
|
||||
// --- 5a. Ensure <updateservers> block with correct URL ---
|
||||
if (preg_match('#<updateservers>\s*</updateservers>#s', $content)) {
|
||||
// Empty updateservers block — inject the server
|
||||
$replacement = "<updateservers>\n"
|
||||
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
||||
. " </updateservers>";
|
||||
$content = preg_replace('#<updateservers>\s*</updateservers>#s', $replacement, $content);
|
||||
$changes[] = 'Added update server URL to empty <updateservers>';
|
||||
} elseif (!str_contains($content, '<updateservers>')) {
|
||||
// No updateservers at all — add before </extension>
|
||||
$serverBlock = "\n <updateservers>\n"
|
||||
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
||||
. " </updateservers>\n";
|
||||
$content = str_replace('</extension>', $serverBlock . '</extension>', $content);
|
||||
$changes[] = 'Added <updateservers> block';
|
||||
} else {
|
||||
// updateservers exists — verify URL is correct
|
||||
if (preg_match('#<server[^>]*>([^<]+)</server>#', $content, $m)) {
|
||||
if ($m[1] !== $updateServerUrl) {
|
||||
$content = preg_replace(
|
||||
'#(<server[^>]*>)[^<]+(</server>)#',
|
||||
"\${1}{$updateServerUrl}\${2}",
|
||||
$content
|
||||
);
|
||||
$changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5b. Ensure <dlid> tag if required ---
|
||||
if ($dlidEnabled) {
|
||||
if (!str_contains($content, '<dlid')) {
|
||||
// Add before <updateservers> if present, otherwise before </extension>
|
||||
$dlidTag = ' <dlid prefix="dlid=" suffix=""/>' . "\n";
|
||||
|
||||
if (str_contains($content, '<updateservers>')) {
|
||||
$content = str_replace('<updateservers>', $dlidTag . "\n <updateservers>", $content);
|
||||
} else {
|
||||
$content = str_replace('</extension>', $dlidTag . '</extension>', $content);
|
||||
}
|
||||
|
||||
$changes[] = 'Added <dlid> tag';
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5c. Ensure <blockChildUninstall> for packages ---
|
||||
if (str_contains($content, 'type="package"') && !str_contains($content, '<blockChildUninstall>')) {
|
||||
$blockTag = ' <blockChildUninstall>true</blockChildUninstall>' . "\n";
|
||||
|
||||
if (str_contains($content, '<dlid')) {
|
||||
// Add after <dlid>
|
||||
$content = preg_replace(
|
||||
'#(<dlid[^/]*/>\s*\n)#',
|
||||
"\${1}{$blockTag}",
|
||||
$content
|
||||
);
|
||||
} elseif (str_contains($content, '<updateservers>')) {
|
||||
$content = str_replace('<updateservers>', $blockTag . "\n <updateservers>", $content);
|
||||
} else {
|
||||
$content = str_replace('</extension>', $blockTag . '</extension>', $content);
|
||||
}
|
||||
|
||||
$changes[] = 'Added <blockChildUninstall>true</blockChildUninstall>';
|
||||
}
|
||||
|
||||
// ── 6. Report and apply ───────────────────────────────────────────
|
||||
if (empty($changes)) {
|
||||
$this->log('INFO', 'All licensing tags are correct — no changes needed');
|
||||
$this->outputResult($ghOutput, 'ok', 'No changes needed');
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change);
|
||||
}
|
||||
|
||||
if ($fix) {
|
||||
file_put_contents($packageManifest, $content);
|
||||
$this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)");
|
||||
$this->outputResult($ghOutput, 'fixed', implode('; ', $changes));
|
||||
} else {
|
||||
$this->log('WARN', 'Run with --fix to apply changes');
|
||||
$this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes));
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write result to $GITHUB_OUTPUT if requested.
|
||||
*/
|
||||
private function outputResult(bool $ghOutput, string $status, string $detail): void
|
||||
{
|
||||
if (!$ghOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
$outputFile = getenv('GITHUB_OUTPUT');
|
||||
|
||||
if ($outputFile === false || $outputFile === '') {
|
||||
echo "licensing_status={$status}\n";
|
||||
echo "licensing_detail={$detail}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$fh = fopen($outputFile, 'a');
|
||||
fwrite($fh, "licensing_status={$status}\n");
|
||||
fwrite($fh, "licensing_detail={$detail}\n");
|
||||
fclose($fh);
|
||||
}
|
||||
}
|
||||
|
||||
$app = new ManifestLicensingCli();
|
||||
exit($app->execute());
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/manifest_read.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption
|
||||
*/
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class PackageBuildCli extends CliFramework
|
||||
{
|
||||
@@ -56,18 +56,13 @@ class PackageBuildCli extends CliFramework
|
||||
}
|
||||
|
||||
// -- Determine source directory -----------------------------------------------
|
||||
$sourceDir = null;
|
||||
foreach (['src', 'htdocs'] as $candidate) {
|
||||
if (is_dir("{$root}/{$candidate}")) {
|
||||
$sourceDir = "{$root}/{$candidate}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||
|
||||
if ($sourceDir === null) {
|
||||
$this->log('ERROR', "No src/ or htdocs/ directory found in {$root}");
|
||||
$this->log('ERROR', "No source/ or src/ directory found in {$root}");
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
|
||||
// -- Determine element and type prefix from manifest --------------------------
|
||||
$extElement = $elementOverride;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/release_cascade.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent.
|
||||
*/
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleaseCreateCli extends CliFramework
|
||||
{
|
||||
@@ -97,8 +97,8 @@ class ReleaseCreateCli extends CliFramework
|
||||
// Find extension manifest (Joomla XML)
|
||||
$extManifest = null;
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
foreach ($manifestFiles as $file) {
|
||||
@@ -112,8 +112,7 @@ class ReleaseCreateCli extends CliFramework
|
||||
// Find Dolibarr module file
|
||||
$modFile = null;
|
||||
$modFiles = array_merge(
|
||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||
);
|
||||
foreach ($modFiles as $file) {
|
||||
|
||||
+28
-9
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleasePackageCli extends CliFramework
|
||||
{
|
||||
@@ -99,9 +99,10 @@ class ReleasePackageCli extends CliFramework
|
||||
$extFolder = '';
|
||||
$typePrefix = '';
|
||||
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
|
||||
@@ -200,14 +201,12 @@ class ReleasePackageCli extends CliFramework
|
||||
}
|
||||
}
|
||||
|
||||
if ($sourceDir === null && is_dir("{$root}/src")) {
|
||||
$sourceDir = "{$root}/src";
|
||||
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
|
||||
$sourceDir = "{$root}/htdocs";
|
||||
if ($sourceDir === null) {
|
||||
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||
}
|
||||
|
||||
if ($sourceDir === null) {
|
||||
echo "No src/ or htdocs/ directory found — skipping package build\n";
|
||||
echo "No source/ or src/ directory found — skipping package build\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -230,12 +229,32 @@ class ReleasePackageCli extends CliFramework
|
||||
$subName = basename($pkgDir);
|
||||
$subZipPath = "{$outputDir}/{$subName}.zip";
|
||||
|
||||
// If sub-package is a full repo checkout (e.g. git submodule),
|
||||
// look for a source/ or src/ subdirectory containing a Joomla manifest XML
|
||||
// and zip that instead of the repo root.
|
||||
$subSourceDir = $pkgDir;
|
||||
$subSrcAbs = SourceResolver::resolveAbsolute($pkgDir);
|
||||
if ($subSrcAbs !== null) {
|
||||
$srcManifests = array_merge(
|
||||
glob("{$subSrcAbs}/*.xml") ?: [],
|
||||
glob("{$subSrcAbs}/pkg_*.xml") ?: []
|
||||
);
|
||||
foreach ($srcManifests as $mf) {
|
||||
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
|
||||
$subSourceDir = $subSrcAbs;
|
||||
$subSrcName = SourceResolver::resolve($pkgDir);
|
||||
echo " Sub-package {$subName}: using {$subSrcName}/ entry-point\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$subZip = new \ZipArchive();
|
||||
if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||
$this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}");
|
||||
continue;
|
||||
}
|
||||
$this->addDirToZip($subZip, $pkgDir, '', $this->excludePatterns);
|
||||
$this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns);
|
||||
$subZip->close();
|
||||
|
||||
$zip->addFile($subZipPath, "packages/{$subName}.zip");
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleasePromoteCli extends CliFramework
|
||||
{
|
||||
@@ -109,8 +109,8 @@ class ReleasePromoteCli extends CliFramework
|
||||
if ($to === 'stable') {
|
||||
$root = realpath($path) ?: $path;
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
foreach ($manifestFiles as $xmlFile) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/release_publish.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
||||
*/
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleaseValidateCli extends CliFramework
|
||||
{
|
||||
@@ -66,8 +66,10 @@ class ReleaseValidateCli extends CliFramework
|
||||
$platform = 'generic';
|
||||
}
|
||||
}
|
||||
$hasSource = is_dir("{$root}/src") || is_dir("{$root}/htdocs");
|
||||
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? 'src/ or htdocs/ found' : 'No src/ or htdocs/ directory');
|
||||
$hasSource = SourceResolver::resolveAbsolute($root) !== null;
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
$srcDirName = SourceResolver::resolve($root);
|
||||
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? "{$srcDirName}/ found" : 'No source/ or src/ directory');
|
||||
if (!file_exists("{$root}/README.md")) {
|
||||
$this->addVResult('README.md', 'FAIL', 'Not found');
|
||||
} else {
|
||||
@@ -109,7 +111,8 @@ class ReleaseValidateCli extends CliFramework
|
||||
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
|
||||
if ($platform === 'joomla') {
|
||||
$manifest = null;
|
||||
foreach (["{$root}/src", $root] as $dir) {
|
||||
$srcAbs = SourceResolver::resolveAbsolute($root);
|
||||
foreach (array_filter([$srcAbs, $root]) as $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
continue;
|
||||
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
|
||||
@@ -156,7 +159,7 @@ class ReleaseValidateCli extends CliFramework
|
||||
}
|
||||
} elseif ($platform === 'dolibarr') {
|
||||
$modFile = null;
|
||||
foreach (['src', 'htdocs'] as $sd) {
|
||||
foreach (SourceResolver::getCandidates() as $sd) {
|
||||
$matches = glob("{$root}/{$sd}/mod*.class.php");
|
||||
if (!empty($matches)) {
|
||||
$modFile = $matches[0];
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/scaffold_client.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
||||
*/
|
||||
|
||||
|
||||
+4
-9
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ThemeLintCli extends CliFramework
|
||||
{
|
||||
@@ -41,17 +41,12 @@ class ThemeLintCli extends CliFramework
|
||||
$errors = 0;
|
||||
$warnings = 0;
|
||||
|
||||
$srcDir = null;
|
||||
foreach (['src', 'htdocs'] as $d) {
|
||||
if (is_dir("{$root}/{$d}")) {
|
||||
$srcDir = "{$root}/{$d}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$srcDir = SourceResolver::resolveAbsolute($root);
|
||||
if ($srcDir === null) {
|
||||
$this->log('ERROR', "No src/ or htdocs/ directory in {$root}");
|
||||
$this->log('ERROR', "No source/ or src/ directory in {$root}");
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
|
||||
echo "Theme Lint: {$srcDir}\n\n";
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class UpdatesXmlBuildCli extends CliFramework
|
||||
{
|
||||
@@ -109,7 +109,7 @@ class UpdatesXmlBuildCli extends CliFramework
|
||||
// -- Locate Joomla manifest ---------------------------------------------------
|
||||
$manifest = null;
|
||||
|
||||
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
||||
$candidates = SourceResolver::globSource($root, 'pkg_*.xml');
|
||||
foreach ($candidates as $f) {
|
||||
if (strpos(file_get_contents($f), '<extension') !== false) {
|
||||
$manifest = $f;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/updates_xml_sync.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* 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
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/version_auto_bump.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
||||
*/
|
||||
|
||||
@@ -109,10 +109,18 @@ class VersionAutoBumpCli extends CliFramework
|
||||
echo "{$line}\n";
|
||||
}
|
||||
|
||||
// Step 2: Read version
|
||||
// Step 2: Read version (--quiet suppresses banner so only the version is output)
|
||||
$versionOutput = [];
|
||||
exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " 2>&1", $versionOutput, $versionRc);
|
||||
$version = trim($versionOutput[0] ?? '');
|
||||
exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " --quiet 2>&1", $versionOutput, $versionRc);
|
||||
// Take the last non-empty line — the version is always the final output
|
||||
$version = '';
|
||||
foreach (array_reverse($versionOutput) as $line) {
|
||||
$line = trim($line);
|
||||
if (preg_match('/^\d{2}\.\d{2}\.\d{2}/', $line)) {
|
||||
$version = $line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($version)) {
|
||||
echo "No version found — skipping\n";
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionBumpCli extends CliFramework
|
||||
{
|
||||
@@ -61,11 +61,12 @@ class VersionBumpCli extends CliFramework
|
||||
}
|
||||
}
|
||||
$manifestVersion = null;
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/mokowaas.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/mokowaas.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
foreach ($manifestFiles as $xmlFile) {
|
||||
@@ -141,7 +142,8 @@ class VersionBumpCli extends CliFramework
|
||||
}
|
||||
}
|
||||
$updatedFiles = [];
|
||||
foreach (["{$root}/src/pkg_*.xml", "{$root}/src/*.xml", "{$root}/src/packages/*/*.xml", "{$root}/*.xml"] as $pattern) {
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
foreach (["{$root}/{$srcName}/pkg_*.xml", "{$root}/{$srcName}/*.xml", "{$root}/{$srcName}/packages/*/*.xml", "{$root}/*.xml"] as $pattern) {
|
||||
foreach (glob($pattern) ?: [] as $xmlFile) {
|
||||
$content = file_get_contents($xmlFile);
|
||||
if (strpos($content, '<extension') === false) {
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionBumpRemoteCli extends CliFramework
|
||||
{
|
||||
@@ -104,11 +104,15 @@ class VersionBumpRemoteCli extends CliFramework
|
||||
$nextVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch);
|
||||
echo "{$version} -> {$nextVersion} ({$branch})\n";
|
||||
|
||||
// Try both source/ and src/ paths for backwards compatibility with remote repos
|
||||
$manifestPaths = [];
|
||||
if ($manifestFile !== null) {
|
||||
$manifestPaths[] = "src/{$manifestFile}";
|
||||
foreach (['source', 'src'] as $srcPrefix) {
|
||||
if ($manifestFile !== null) {
|
||||
$manifestPaths[] = "{$srcPrefix}/{$manifestFile}";
|
||||
}
|
||||
$manifestPaths[] = "{$srcPrefix}/templateDetails.xml";
|
||||
$manifestPaths[] = "{$srcPrefix}/manifest.xml";
|
||||
}
|
||||
$manifestPaths = array_merge($manifestPaths, ['src/templateDetails.xml', 'src/manifest.xml']);
|
||||
$manifestUpdated = false;
|
||||
foreach ($manifestPaths as $mPath) {
|
||||
$result = $this->updateRemoteFile($apiBase, $token, $mPath, $branch, function (string $content) use ($version, $nextVersion): string {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/version_check.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Validate version consistency across README, manifests, and sub-packages
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionCheckCli extends CliFramework
|
||||
{
|
||||
@@ -77,7 +77,8 @@ class VersionCheckCli extends CliFramework
|
||||
$versions['pyproject.toml'] = $m[1];
|
||||
}
|
||||
}
|
||||
foreach (["{$root}/src/pkg_*.xml", "{$root}/src/*.xml", "{$root}/src/packages/*/*.xml", "{$root}/*.xml"] as $glob) {
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
foreach (["{$root}/{$srcName}/pkg_*.xml", "{$root}/{$srcName}/*.xml", "{$root}/{$srcName}/packages/*/*.xml", "{$root}/*.xml"] as $glob) {
|
||||
foreach (glob($glob) ?: [] as $file) {
|
||||
if (basename($file) === 'updates.xml') {
|
||||
continue;
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionReadCli extends CliFramework
|
||||
{
|
||||
@@ -64,9 +64,9 @@ class VersionReadCli extends CliFramework
|
||||
// -- 3. Fallback: Joomla manifest XML --
|
||||
$manifestVersion = null;
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionSetPlatformCli extends CliFramework
|
||||
{
|
||||
@@ -53,6 +53,12 @@ class VersionSetPlatformCli extends CliFramework
|
||||
// Strip any existing suffix(es) before applying the correct one
|
||||
$version = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version);
|
||||
|
||||
// Validate version format — must be XX.YY.ZZ to prevent XML corruption
|
||||
if (!preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $version)) {
|
||||
$this->log('ERROR', "Invalid version format: '{$version}' — expected XX.YY.ZZ");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Append stability suffix for non-stable releases
|
||||
$stabilitySuffixMap = [
|
||||
'stable' => '',
|
||||
@@ -104,7 +110,8 @@ class VersionSetPlatformCli extends CliFramework
|
||||
|
||||
// Dolibarr: $this->version + $this->url_last_version in mod*.class.php
|
||||
if ($platform === 'crm-module') {
|
||||
$pattern = "{$root}/src/core/modules/mod*.class.php";
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
$pattern = "{$root}/{$srcName}/core/modules/mod*.class.php";
|
||||
foreach (glob($pattern) ?: [] as $file) {
|
||||
$content = file_get_contents($file);
|
||||
|
||||
@@ -140,9 +147,10 @@ class VersionSetPlatformCli extends CliFramework
|
||||
|
||||
// Joomla: <version> in XML manifests (top-level + sub-packages)
|
||||
if (in_array($platform, ['waas-component', 'joomla'], true)) {
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
$xmlFiles = array_merge(
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
glob("{$root}/{$srcName}/*.xml") ?: [],
|
||||
glob("{$root}/{$srcName}/packages/*/*.xml") ?: [],
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
if (empty($xmlFiles)) {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/wiki_sync.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Sync select wiki pages from moko-platform to all template repos
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /deploy/backup-before-deploy.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /deploy/deploy-dolibarr.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
|
||||
*/
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
@@ -51,9 +51,9 @@ class DeploySftp extends CliFramework
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Deploy a repository src/ directory to a remote web server via SFTP');
|
||||
$this->setDescription('Deploy a repository source directory to a remote web server via SFTP');
|
||||
$this->addArgument('--path', 'Repository root (default: current directory)', '.');
|
||||
$this->addArgument('--src-dir', 'Source sub-directory to upload (default: src)', 'src');
|
||||
$this->addArgument('--src-dir', 'Source sub-directory to upload (default: auto-detect)', '');
|
||||
$this->addArgument('--env', 'Target environment: dev or rs', '');
|
||||
$this->addArgument('--config', 'Explicit config file path — overrides --env', '');
|
||||
$this->addArgument('--key-passphrase', 'Passphrase for the SSH private key', '');
|
||||
@@ -158,7 +158,8 @@ class DeploySftp extends CliFramework
|
||||
*/
|
||||
private function resolveSrcDir(string $repoPath): string
|
||||
{
|
||||
$sub = $this->getArgument('--src-dir', 'src');
|
||||
$sub = $this->getArgument('--src-dir', '') ?: SourceResolver::resolve($repoPath);
|
||||
SourceResolver::warnIfLegacy($repoPath);
|
||||
$dir = $repoPath . DIRECTORY_SEPARATOR . $sub;
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /deploy/health-check.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Post-deploy health check — verify a Joomla site is responding correctly
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /deploy/rollback-joomla.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /deploy/sync-joomla.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Sync Joomla site directories between two servers via rsync over SSH
|
||||
*/
|
||||
|
||||
|
||||
@@ -141,6 +141,26 @@ abstract class CliFramework
|
||||
/** @var float Script start time for elapsed-time reporting. */
|
||||
private float $startTime;
|
||||
|
||||
// =========================================================================
|
||||
// Display output — all decorative output goes to stderr
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Write decorative/diagnostic output to stderr.
|
||||
*
|
||||
* All non-data output (banners, progress bars, section headers, status
|
||||
* lines, log messages) MUST use this method so that stdout is reserved
|
||||
* for machine-readable data. This ensures that shell captures like
|
||||
* VERSION=$(php version_read.php --path .)
|
||||
* only receive the actual data, not decorative text.
|
||||
*
|
||||
* @since 04.00.16
|
||||
*/
|
||||
protected function display(string $text): void
|
||||
{
|
||||
fwrite(STDERR, $text);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Constructor
|
||||
// =========================================================================
|
||||
@@ -151,6 +171,13 @@ abstract class CliFramework
|
||||
*/
|
||||
public function __construct(string $name = '', string $version = '04.00.15')
|
||||
{
|
||||
// Load Composer autoloader for Enterprise classes (SourceResolver, etc.)
|
||||
$autoloader = __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
if (file_exists($autoloader)) {
|
||||
require_once $autoloader;
|
||||
}
|
||||
|
||||
$this->scriptName = $name ?: basename($_SERVER['argv'][0] ?? 'script', '.php');
|
||||
$this->scriptVersion = $version;
|
||||
$this->startTime = microtime(true);
|
||||
@@ -326,14 +353,14 @@ abstract class CliFramework
|
||||
protected function printHelp(): void
|
||||
{
|
||||
$w = $this->termWidth();
|
||||
echo $this->c(self::C_BOLD . self::C_CYAN, $this->scriptName);
|
||||
$this->display($this->c(self::C_BOLD . self::C_CYAN, $this->scriptName));
|
||||
if ($this->description !== '') {
|
||||
echo ' — ' . $this->description;
|
||||
$this->display(' — ' . $this->description);
|
||||
}
|
||||
echo "\n";
|
||||
echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n";
|
||||
echo $this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n";
|
||||
echo $this->c(self::C_BOLD, 'Options:') . "\n";
|
||||
$this->display("\n");
|
||||
$this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n");
|
||||
$this->display($this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n");
|
||||
$this->display($this->c(self::C_BOLD, 'Options:') . "\n");
|
||||
|
||||
$builtIn = [
|
||||
'--help' => ['desc' => 'Show this help message', 'default' => null],
|
||||
@@ -348,16 +375,16 @@ abstract class CliFramework
|
||||
$hint = ($default !== null && $default !== false)
|
||||
? $this->c(self::C_DIM, " (default: {$default})")
|
||||
: '';
|
||||
printf(
|
||||
$this->display(sprintf(
|
||||
" %s%-22s%s%s%s\n",
|
||||
self::C_CYAN,
|
||||
$name,
|
||||
self::C_RESET,
|
||||
$def['desc'],
|
||||
$hint
|
||||
);
|
||||
));
|
||||
}
|
||||
echo "\n";
|
||||
$this->display("\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -378,23 +405,23 @@ abstract class CliFramework
|
||||
$titleLine = $this->padRight($titleStyled, $inner, strlen($titleRaw));
|
||||
$descLine = ($desc !== '') ? $this->padRight(" {$desc}", $inner) : null;
|
||||
|
||||
echo "\n";
|
||||
echo $this->c(
|
||||
$this->display("\n");
|
||||
$this->display($this->c(
|
||||
self::C_CYAN,
|
||||
self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR
|
||||
) . "\n";
|
||||
echo $this->c(self::C_CYAN, self::BOX_V)
|
||||
) . "\n");
|
||||
$this->display($this->c(self::C_CYAN, self::BOX_V)
|
||||
. $this->c(self::C_BOLD, $titleLine)
|
||||
. $this->c(self::C_CYAN, self::BOX_V) . "\n";
|
||||
. $this->c(self::C_CYAN, self::BOX_V) . "\n");
|
||||
if ($descLine !== null) {
|
||||
echo $this->c(self::C_CYAN, self::BOX_V)
|
||||
$this->display($this->c(self::C_CYAN, self::BOX_V)
|
||||
. $this->c(self::C_DIM, $descLine)
|
||||
. $this->c(self::C_CYAN, self::BOX_V) . "\n";
|
||||
. $this->c(self::C_CYAN, self::BOX_V) . "\n");
|
||||
}
|
||||
echo $this->c(
|
||||
$this->display($this->c(
|
||||
self::C_CYAN,
|
||||
self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR
|
||||
) . "\n\n";
|
||||
) . "\n\n");
|
||||
}
|
||||
|
||||
/** Print the dry-run notice box. */
|
||||
@@ -403,18 +430,18 @@ abstract class CliFramework
|
||||
$w = min($this->termWidth(), 70);
|
||||
$msg = ' ' . self::ICON_DRY . ' DRY-RUN MODE — no changes will be written ';
|
||||
$row = $this->padRight($msg, $w - 2);
|
||||
echo $this->c(
|
||||
$this->display($this->c(
|
||||
self::C_YELLOW . self::C_BOLD,
|
||||
self::BOX_TL . str_repeat(self::BOX_H, $w - 2) . self::BOX_TR
|
||||
) . "\n";
|
||||
echo $this->c(
|
||||
) . "\n");
|
||||
$this->display($this->c(
|
||||
self::C_YELLOW . self::C_BOLD,
|
||||
self::BOX_V . $row . self::BOX_V
|
||||
) . "\n";
|
||||
echo $this->c(
|
||||
) . "\n");
|
||||
$this->display($this->c(
|
||||
self::C_YELLOW . self::C_BOLD,
|
||||
self::BOX_BL . str_repeat(self::BOX_H, $w - 2) . self::BOX_BR
|
||||
) . "\n\n";
|
||||
) . "\n\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -435,11 +462,11 @@ abstract class CliFramework
|
||||
$w = $this->termWidth();
|
||||
$text = " {$title} ";
|
||||
$fill = max(0, $w - strlen($text) - 4);
|
||||
echo "\n";
|
||||
echo $this->c(
|
||||
$this->display("\n");
|
||||
$this->display($this->c(
|
||||
self::C_CYAN,
|
||||
str_repeat(self::BOX_H, 2) . $text . str_repeat(self::BOX_H, $fill)
|
||||
) . "\n\n";
|
||||
) . "\n\n");
|
||||
}
|
||||
|
||||
/** Print a plain horizontal divider. */
|
||||
@@ -449,7 +476,7 @@ abstract class CliFramework
|
||||
return;
|
||||
}
|
||||
$this->clearProgress();
|
||||
echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n";
|
||||
$this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -495,11 +522,7 @@ abstract class CliFramework
|
||||
|
||||
$line = "{$ts} {$icon} {$badge} {$text}\n";
|
||||
|
||||
if ($level === 'ERROR') {
|
||||
fwrite(STDERR, $line);
|
||||
} else {
|
||||
echo $line;
|
||||
}
|
||||
$this->display($line);
|
||||
}
|
||||
|
||||
/** Log a success message. */
|
||||
@@ -564,7 +587,7 @@ abstract class CliFramework
|
||||
? ' ' . $this->c(self::C_DIM, "— {$detail}")
|
||||
: '';
|
||||
|
||||
echo ' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n";
|
||||
$this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -601,10 +624,10 @@ abstract class CliFramework
|
||||
$line = " [{$bar}] {$percent} {$counter}{$suffix}";
|
||||
|
||||
if ($newline) {
|
||||
echo "\r{$line}\n";
|
||||
$this->display("\r{$line}\n");
|
||||
$this->progressActive = false;
|
||||
} else {
|
||||
echo "\r{$line}";
|
||||
$this->display("\r{$line}");
|
||||
$this->progressActive = true;
|
||||
}
|
||||
}
|
||||
@@ -613,7 +636,7 @@ abstract class CliFramework
|
||||
protected function clearProgress(): void
|
||||
{
|
||||
if ($this->progressActive) {
|
||||
echo "\r" . str_repeat(' ', $this->termWidth()) . "\r";
|
||||
$this->display("\r" . str_repeat(' ', $this->termWidth()) . "\r");
|
||||
$this->progressActive = false;
|
||||
}
|
||||
}
|
||||
@@ -644,8 +667,8 @@ abstract class CliFramework
|
||||
$maxKey = max(array_map('strlen', array_keys($rows)));
|
||||
$inner = $maxKey + 20;
|
||||
|
||||
echo "\n";
|
||||
echo $this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n";
|
||||
$this->display("\n");
|
||||
$this->display($this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n");
|
||||
|
||||
foreach ($rows as $label => $value) {
|
||||
$valStr = (string) $value;
|
||||
@@ -653,10 +676,10 @@ abstract class CliFramework
|
||||
$padding = $inner - strlen($label) - $valVis - 4;
|
||||
$row = ' ' . $this->c(self::C_BOLD, $label)
|
||||
. str_repeat(' ', max(1, $padding)) . $valStr . ' ';
|
||||
echo $this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n";
|
||||
$this->display($this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n");
|
||||
}
|
||||
|
||||
echo $this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n";
|
||||
$this->display($this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -702,7 +725,7 @@ abstract class CliFramework
|
||||
$this->clearProgress();
|
||||
$badge = $this->c(self::C_BOLD . self::C_MAGENTA, "Step {$current}/{$total}");
|
||||
$arrow = $this->c(self::C_DIM, self::ICON_INFO);
|
||||
echo "\n{$badge} {$arrow} {$title}\n";
|
||||
$this->display("\n{$badge} {$arrow} {$title}\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -964,13 +987,13 @@ abstract class CliFramework
|
||||
}
|
||||
|
||||
// Header.
|
||||
echo $sep . "\n";
|
||||
$this->display($sep . "\n");
|
||||
$headerLine = '|';
|
||||
foreach ($headers as $i => $h) {
|
||||
$headerLine .= ' ' . $this->c(self::C_BOLD, str_pad($h, $widths[$i])) . ' |';
|
||||
}
|
||||
echo $headerLine . "\n";
|
||||
echo $sep . "\n";
|
||||
$this->display($headerLine . "\n");
|
||||
$this->display($sep . "\n");
|
||||
|
||||
// Rows.
|
||||
foreach ($rows as $row) {
|
||||
@@ -978,9 +1001,9 @@ abstract class CliFramework
|
||||
foreach ($row as $i => $cell) {
|
||||
$line .= ' ' . str_pad((string) $cell, $widths[$i]) . ' |';
|
||||
}
|
||||
echo $line . "\n";
|
||||
$this->display($line . "\n");
|
||||
}
|
||||
echo $sep . "\n";
|
||||
$this->display($sep . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,31 +147,29 @@ class ManifestReader
|
||||
/**
|
||||
* 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. 'src', 'htdocs')
|
||||
* @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. src/index.ts → src)
|
||||
// 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: check common directories
|
||||
// Fallback: use SourceResolver (source/ → src/ → htdocs/ → default 'source')
|
||||
if ($root !== '') {
|
||||
if (is_dir("{$root}/src")) {
|
||||
return 'src';
|
||||
}
|
||||
if (is_dir("{$root}/htdocs")) {
|
||||
return 'htdocs';
|
||||
}
|
||||
return SourceResolver::resolve($root);
|
||||
}
|
||||
|
||||
return 'src';
|
||||
return 'source';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,8 @@ class PackageBuilder
|
||||
mkdir($packageDir, 0755, true);
|
||||
mkdir($distDir, 0755, true);
|
||||
|
||||
foreach (['src', 'admin', 'site'] as $dir) {
|
||||
$srcName = SourceResolver::resolve($repoRoot);
|
||||
foreach ([$srcName, 'admin', 'site'] as $dir) {
|
||||
if (is_dir($repoRoot . '/' . $dir)) {
|
||||
self::copyDirectory($repoRoot . '/' . $dir, $packageDir . '/' . $dir);
|
||||
}
|
||||
@@ -94,15 +95,15 @@ class PackageBuilder
|
||||
/**
|
||||
* Build a Dolibarr module release package.
|
||||
*
|
||||
* Copies everything under src/ into a build staging directory and archives
|
||||
* it as dist/<MODULE_NAME>_<VERSION>.zip.
|
||||
* Copies everything under source/ (or src/) into a build staging directory
|
||||
* and archives it as dist/<MODULE_NAME>_<VERSION>.zip.
|
||||
*
|
||||
* @param string $repoRoot Absolute path to the repository root.
|
||||
* @param string $moduleName Module name (used in archive filename).
|
||||
* @param string $version Version string.
|
||||
* @param bool $dryRun When true, preview without writing.
|
||||
* @return string Path to the created archive (or would-create path in dry-run).
|
||||
* @throws \RuntimeException When src/ is absent or archive creation fails.
|
||||
* @throws \RuntimeException When source directory is absent or archive creation fails.
|
||||
*/
|
||||
public static function buildDolibarr(
|
||||
string $repoRoot,
|
||||
@@ -110,14 +111,15 @@ class PackageBuilder
|
||||
string $version,
|
||||
bool $dryRun = false
|
||||
): string {
|
||||
$srcDir = $repoRoot . '/src';
|
||||
$srcDir = SourceResolver::resolveAbsolute($repoRoot);
|
||||
$buildDir = $repoRoot . '/build';
|
||||
$distDir = $repoRoot . '/dist';
|
||||
$archivePath = $distDir . '/' . $moduleName . '_' . $version . '.zip';
|
||||
|
||||
if (!is_dir($srcDir)) {
|
||||
throw new \RuntimeException("src/ directory not found at {$srcDir}");
|
||||
if ($srcDir === null) {
|
||||
throw new \RuntimeException("source/ or src/ directory not found in {$repoRoot}");
|
||||
}
|
||||
SourceResolver::warnIfLegacy($repoRoot);
|
||||
|
||||
if ($dryRun) {
|
||||
return $archivePath;
|
||||
|
||||
@@ -20,6 +20,7 @@ declare(strict_types=1);
|
||||
namespace MokoEnterprise\Plugins;
|
||||
|
||||
use MokoEnterprise\AbstractProjectPlugin;
|
||||
use MokoEnterprise\SourceResolver;
|
||||
|
||||
/**
|
||||
* MCP Server Project Plugin
|
||||
@@ -55,10 +56,12 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
$warnings = [];
|
||||
|
||||
// Check for required source files
|
||||
$requiredSrc = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
||||
$srcName = SourceResolver::resolve($projectPath);
|
||||
SourceResolver::warnIfLegacy($projectPath);
|
||||
$requiredSrc = ['index.ts', 'client.ts', 'config.ts', 'types.ts'];
|
||||
foreach ($requiredSrc as $file) {
|
||||
if (!file_exists("{$projectPath}/{$file}")) {
|
||||
$errors[] = "Missing required source file: {$file}";
|
||||
if (SourceResolver::findUnderSource($projectPath, $file) === null) {
|
||||
$errors[] = "Missing required source file: {$srcName}/{$file}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,37 +85,33 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
$errors[] = 'Missing tsconfig.json';
|
||||
}
|
||||
|
||||
// Check for setup wizard
|
||||
if (!file_exists("{$projectPath}/scripts/setup.mjs")) {
|
||||
$warnings[] = 'Missing scripts/setup.mjs — interactive setup wizard recommended';
|
||||
}
|
||||
|
||||
// Check for config example
|
||||
if (!file_exists("{$projectPath}/config.example.json")) {
|
||||
$warnings[] = 'Missing config.example.json — example configuration recommended';
|
||||
}
|
||||
|
||||
// Check for shebang in index.ts
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
$indexTs = SourceResolver::findUnderSource($projectPath, 'index.ts');
|
||||
if ($indexTs !== null) {
|
||||
$content = @file_get_contents($indexTs);
|
||||
if ($content && strpos($content, '#!/usr/bin/env node') === false) {
|
||||
$warnings[] = 'src/index.ts should start with #!/usr/bin/env node shebang';
|
||||
$warnings[] = "{$srcName}/index.ts should start with #!/usr/bin/env node shebang";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for McpServer usage
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
if ($indexTs !== null) {
|
||||
$content = $content ?? @file_get_contents($indexTs);
|
||||
if ($content && strpos($content, 'McpServer') === false) {
|
||||
$errors[] = 'src/index.ts must import and use McpServer from @modelcontextprotocol/sdk';
|
||||
$errors[] = "{$srcName}/index.ts must import and use McpServer from @modelcontextprotocol/sdk";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for StdioServerTransport
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
if ($indexTs !== null) {
|
||||
$content = $content ?? @file_get_contents($indexTs);
|
||||
if ($content && strpos($content, 'StdioServerTransport') === false) {
|
||||
$warnings[] = 'src/index.ts should use StdioServerTransport for Claude Code compatibility';
|
||||
$warnings[] = "{$srcName}/index.ts should use StdioServerTransport for Claude Code compatibility";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +189,13 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
$score = 100;
|
||||
|
||||
// Check for required source files
|
||||
$requiredSrc = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
||||
$srcName = SourceResolver::resolve($projectPath);
|
||||
$requiredSrc = ['index.ts', 'client.ts', 'config.ts', 'types.ts'];
|
||||
foreach ($requiredSrc as $file) {
|
||||
if (!file_exists("{$projectPath}/{$file}")) {
|
||||
if (SourceResolver::findUnderSource($projectPath, $file) === null) {
|
||||
$issues[] = [
|
||||
'severity' => 'critical',
|
||||
'message' => "Missing required file: {$file}",
|
||||
'message' => "Missing required file: {$srcName}/{$file}",
|
||||
];
|
||||
$score -= 20;
|
||||
}
|
||||
@@ -214,14 +214,15 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
}
|
||||
|
||||
// Check for at least one registered tool
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
$indexTs = SourceResolver::findUnderSource($projectPath, 'index.ts');
|
||||
if ($indexTs !== null) {
|
||||
$content = @file_get_contents($indexTs);
|
||||
if ($content) {
|
||||
$toolCount = substr_count($content, 'server.tool(');
|
||||
if ($toolCount === 0) {
|
||||
$issues[] = [
|
||||
'severity' => 'critical',
|
||||
'message' => 'No MCP tools registered in src/index.ts',
|
||||
'message' => "No MCP tools registered in {$srcName}/index.ts",
|
||||
];
|
||||
$score -= 25;
|
||||
} elseif ($toolCount < 5) {
|
||||
|
||||
@@ -173,37 +173,15 @@ class RepositorySynchronizer
|
||||
$platform = $this->detectPlatform($repoInfo);
|
||||
$this->logger->logInfo("Detected platform for {$repo}: {$platform}");
|
||||
|
||||
// Load file list from the Terraform definition for this platform
|
||||
$filesToSync = $this->definitionParser->parseForPlatform($platform, $repoRoot);
|
||||
|
||||
// Append shared workflows — the parser can't extract them from nested
|
||||
// subdirectories blocks due to heredoc interference in .tf files.
|
||||
$sharedFiles = $this->getSharedWorkflows($platform, $repoRoot);
|
||||
|
||||
// Deduplicate by destination — shared workflows take precedence over parser entries
|
||||
$seen = [];
|
||||
foreach ($filesToSync as $f) {
|
||||
$seen[$f['destination']] = true;
|
||||
}
|
||||
foreach ($sharedFiles as $f) {
|
||||
if (!isset($seen[$f['destination']])) {
|
||||
$filesToSync[] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
$defCount = count($filesToSync) - count($sharedFiles);
|
||||
$sharedAdded = count($filesToSync) - $defCount;
|
||||
$sharedTotal = count($sharedFiles);
|
||||
// Load shared workflows and config files for this platform from templates
|
||||
$filesToSync = $this->getSharedWorkflows($platform, $repoRoot);
|
||||
$sharedTotal = count($filesToSync);
|
||||
$this->logger->logInfo(
|
||||
"Loaded " . count($filesToSync) . " sync entries for {$platform}"
|
||||
. " (def={$defCount}, shared={$sharedAdded}/{$sharedTotal} added, "
|
||||
. ($sharedTotal - $sharedAdded) . " deduped)"
|
||||
"Loaded {$sharedTotal} sync entries for {$platform}"
|
||||
);
|
||||
// Log shared workflow destinations for debugging
|
||||
foreach ($sharedFiles as $sf) {
|
||||
foreach ($filesToSync as $sf) {
|
||||
$dest = $sf['destination'] ?? '?';
|
||||
$added = !isset($seen[$dest]) ? 'ADDED' : 'DEDUPED';
|
||||
$this->logger->logInfo(" shared: {$dest} [{$added}]");
|
||||
$this->logger->logInfo(" sync: {$dest}");
|
||||
}
|
||||
|
||||
if (empty($filesToSync)) {
|
||||
@@ -1380,7 +1358,7 @@ class RepositorySynchronizer
|
||||
|
||||
$descriptors = array_values(array_filter(
|
||||
$paths,
|
||||
static fn(string $p): bool => (bool) preg_match('#src/core/modules/mod\w+\.class\.php$#', $p)
|
||||
static fn(string $p): bool => (bool) preg_match('#(?:source|src)/core/modules/mod\w+\.class\.php$#', $p)
|
||||
));
|
||||
|
||||
if (empty($descriptors)) {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: MokoPlatform.Enterprise
|
||||
* INGROUP: MokoPlatform.Lib
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /lib/Enterprise/SourceResolver.php
|
||||
* BRIEF: Resolve the root-level source directory across repos (source/, src/, htdocs/)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MokoEnterprise;
|
||||
|
||||
/**
|
||||
* Source Directory Resolver
|
||||
*
|
||||
* Provides a single, consistent fallback chain for locating the root-level
|
||||
* source directory in any MokoStandards repository. The preferred directory
|
||||
* is `source/`, with legacy `src/` and `htdocs/` as fallbacks.
|
||||
*
|
||||
* This class exists because Joomla extensions use `src/` for namespace
|
||||
* autoloading (e.g. administrator/components/com_foo/src/). Renaming our
|
||||
* root-level source directory to `source/` avoids that collision. During
|
||||
* the transition period, repos may still use `src/`, so all tooling must
|
||||
* check both.
|
||||
*
|
||||
* Usage:
|
||||
* $dir = SourceResolver::resolve($repoRoot); // 'source', 'src', or 'htdocs'
|
||||
* $abs = SourceResolver::resolveAbsolute($repoRoot); // full path or null
|
||||
* $xmls = SourceResolver::globSource($repoRoot, '*.xml'); // glob under first match
|
||||
* $path = SourceResolver::findUnderSource($repoRoot, 'core/modules'); // subpath lookup
|
||||
*
|
||||
* @since 09.02.00
|
||||
*/
|
||||
class SourceResolver
|
||||
{
|
||||
/**
|
||||
* Ordered candidate directories. source/ is preferred, src/ is legacy fallback.
|
||||
*
|
||||
* When the migration is complete and all repos use source/, the 'src'
|
||||
* entry can be removed from this list.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private const CANDIDATES = ['source', 'src', 'htdocs'];
|
||||
|
||||
/**
|
||||
* Resolve the source directory name for a repository root.
|
||||
*
|
||||
* Returns the first candidate directory that exists, or 'source' as the
|
||||
* default when no candidate is found (e.g. for new repos being scaffolded).
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @return string Directory name (e.g. 'source', 'src', 'htdocs').
|
||||
*/
|
||||
public static function resolve(string $root): string
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
if (is_dir("{$root}/{$candidate}")) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return 'source';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the source directory as an absolute path.
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @return string|null Absolute path to the source directory, or null if none exists.
|
||||
*/
|
||||
public static function resolveAbsolute(string $root): ?string
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
$path = "{$root}/{$candidate}";
|
||||
if (is_dir($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glob for files under the source directory.
|
||||
*
|
||||
* Checks each candidate directory in order and returns matches from the
|
||||
* first candidate that produces results. This replaces patterns like:
|
||||
*
|
||||
* glob("{$root}/src/*.xml")
|
||||
*
|
||||
* With the backwards-compatible:
|
||||
*
|
||||
* SourceResolver::globSource($root, '*.xml')
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @param string $pattern Glob pattern relative to the source directory.
|
||||
* @return string[] Matched file paths (may be empty).
|
||||
*/
|
||||
public static function globSource(string $root, string $pattern): array
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
$dir = "{$root}/{$candidate}";
|
||||
if (!is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
$matches = glob("{$dir}/{$pattern}") ?: [];
|
||||
if ($matches !== []) {
|
||||
return $matches;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a subpath under any source directory candidate.
|
||||
*
|
||||
* Useful for locating platform-specific subdirectories like
|
||||
* `core/modules/` (Dolibarr) or `media/templates/` (Joomla client themes)
|
||||
* regardless of whether the repo uses `source/` or `src/`.
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @param string $subpath Relative path to look for (e.g. 'core/modules', 'index.ts').
|
||||
* @return string|null Absolute path if found, null otherwise.
|
||||
*/
|
||||
public static function findUnderSource(string $root, string $subpath): ?string
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
$full = "{$root}/{$candidate}/{$subpath}";
|
||||
if (file_exists($full) || is_dir($full)) {
|
||||
return $full;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ordered list of candidate directory names.
|
||||
*
|
||||
* Useful for workflows or scripts that need to iterate candidates
|
||||
* themselves (e.g. building find/grep patterns).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getCandidates(): array
|
||||
{
|
||||
return self::CANDIDATES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the resolved source directory is a legacy name (src/).
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @return bool True if the repo uses src/ instead of source/.
|
||||
*/
|
||||
public static function isLegacy(string $root): bool
|
||||
{
|
||||
$resolved = self::resolve($root);
|
||||
|
||||
return $resolved === 'src';
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a deprecation warning to stderr if the repo still uses src/.
|
||||
*
|
||||
* CLI tools should call this after resolving the source directory so
|
||||
* that maintainers know to rename src/ → source/.
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
*/
|
||||
public static function warnIfLegacy(string $root): void
|
||||
{
|
||||
if (self::isLegacy($root)) {
|
||||
fwrite(STDERR, "⚠ WARNING: This repo uses src/ which is deprecated. Rename to source/ per MokoStandards.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoPlatform.Documentation
|
||||
INGROUP: MokoPlatform.Templates
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
PATH: /templates/docs/dolibarr/update-server.md
|
||||
BRIEF: Developer guide for wiring up Dolibarr module update checks — synced to docs/ in all CRM repos
|
||||
-->
|
||||
|
||||
# Module Update Server
|
||||
|
||||
This module uses `update.txt` hosted in the repo root to enable Dolibarr's built-in update checker.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. When a PR is merged to `main`, the `auto-release.yml` workflow:
|
||||
- Reads the version from `README.md`
|
||||
- Sets `$this->version` in the module descriptor to the real version
|
||||
- Creates a GitHub Release with a git tag
|
||||
- Writes `update.txt` to the repo root
|
||||
2. The module descriptor's `$this->url_last_version` points to the raw `update.txt` URL
|
||||
3. Dolibarr's admin panel fetches this URL to check for available updates
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Module Descriptor
|
||||
|
||||
In `src/core/modules/mod*.class.php`, ensure these lines are in the constructor:
|
||||
|
||||
```php
|
||||
// Version — 'development' on dev branches, real version set by auto-release on merge to main
|
||||
$this->version = 'development';
|
||||
|
||||
// Update check — points to update.txt written by auto-release workflow
|
||||
$this->url_last_version = 'https://raw.githubusercontent.com/mokoconsulting-tech/REPO_NAME/main/update.txt';
|
||||
```
|
||||
|
||||
Replace `REPO_NAME` with this repository's name.
|
||||
|
||||
### 2. Version Parser
|
||||
|
||||
Add this method to the module descriptor class to parse the JSON response:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Get the latest available version from the update server.
|
||||
*
|
||||
* Reads update.txt from the GitHub repo and extracts the version field.
|
||||
* Called by Dolibarr's module update checker.
|
||||
*
|
||||
* @return string Latest version number, or empty string on failure
|
||||
*/
|
||||
public function getLatestVersion(): string
|
||||
{
|
||||
if (empty($this->url_last_version)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$content = @file_get_contents($this->url_last_version);
|
||||
if ($content === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
return $data['version'] ?? '';
|
||||
}
|
||||
```
|
||||
|
||||
### 3. That's It
|
||||
|
||||
Everything else is automated:
|
||||
- `deploy-dev.yml` sets version to `"development"` on dev branches
|
||||
- `auto-release.yml` sets the real version and writes `update.txt` on release
|
||||
- `sync-version-on-merge.yml` bumps the patch version in README.md
|
||||
|
||||
## update.txt Format
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "01.02.03",
|
||||
"tag": "v01.02.03",
|
||||
"repo": "mokoconsulting-tech/REPO_NAME",
|
||||
"release_url": "https://git.mokoconsulting.tech/mokoconsulting-tech/REPO_NAME/releases/tag/v01.02.03",
|
||||
"updated": "2026-03-27T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
> **Do not edit `update.txt` manually** — it is auto-generated by the release workflow.
|
||||
|
||||
## Version Lifecycle
|
||||
|
||||
```
|
||||
dev/** branch → $this->version = "development" (no update.txt)
|
||||
merge to main → $this->version = "01.02.03" → update.txt written → GitHub Release
|
||||
next commit → README auto-bumps to 01.02.04 (no release yet)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| `update.txt` doesn't exist | Merge a PR to main — the first release creates it |
|
||||
| Version shows "development" | Expected on `dev/**` branches — real version set on release |
|
||||
| Dolibarr doesn't detect updates | Check `$this->url_last_version` URL returns valid JSON |
|
||||
| Wrong version in update.txt | Check README.md VERSION field — it's the source of truth |
|
||||
|
||||
### Test the URL
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/mokoconsulting-tech/REPO_NAME/main/update.txt | jq .
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: {{REPO_NAME}}.Documentation
|
||||
INGROUP: MokoPlatform.Templates
|
||||
REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: {{standards_version}}
|
||||
BRIEF: How this module's update server file (update.txt) is managed
|
||||
-->
|
||||
|
||||
# Dolibarr Update Server
|
||||
|
||||
[](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)
|
||||
|
||||
This document explains how `update.txt` is automatically managed for this Dolibarr module.
|
||||
|
||||
## How It Works
|
||||
|
||||
Dolibarr checks for module updates by fetching a plain-text file from the URL in `$this->url_last_version` in the module descriptor (`src/core/modules/mod*.class.php`). The file must contain **only the version string** — no JSON, no XML, no trailing newline.
|
||||
|
||||
### Automatic Generation
|
||||
|
||||
| Event | Workflow | `update.txt` Content | `$this->version` |
|
||||
|-------|----------|---------------------|-------------------|
|
||||
| Merge to `main` | `auto-release.yml` | `XX.YY.ZZ` (real version) | Real version |
|
||||
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` |
|
||||
| Push to `rc/**` | `deploy-dev.yml` | `XX.YY.ZZ-rc` | RC version |
|
||||
|
||||
### Module Descriptor
|
||||
|
||||
The `url_last_version` in your module descriptor should point to:
|
||||
|
||||
```
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/update.txt
|
||||
```
|
||||
|
||||
This is set automatically by `version_set_platform.php` during the build pipeline. **Never manually edit `$this->version` or `$this->url_last_version`** — the workflows handle it.
|
||||
|
||||
### Branch Lifecycle
|
||||
|
||||
```
|
||||
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX
|
||||
(development) (release candidate) (stable release) (frozen snapshot)
|
||||
```
|
||||
|
||||
1. **Development** (`dev/**`): `update.txt` = `development`, `$this->version` = `development`
|
||||
2. **Release Candidate** (`rc/**`): `update.txt` = `XX.YY.ZZ-rc`, version set to RC
|
||||
3. **Stable Release** (merge to `main`): `auto-release.yml` writes real version to `update.txt`, creates GitHub Release + tag, creates `version/XX` branch
|
||||
4. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed
|
||||
|
||||
### Health Checks
|
||||
|
||||
The `repo_health.yml` workflow verifies on every commit:
|
||||
|
||||
- `update.txt` exists in the repository root
|
||||
- Module descriptor (`mod*.class.php`) exists in `src/core/modules/`
|
||||
- `$this->numero` is set and non-zero
|
||||
- `$this->version` is not hardcoded (should be set by workflow)
|
||||
- `url_last_version` points to `update.txt` (not `update.json`)
|
||||
- `url_last_version` references `/main/` branch on the main branch
|
||||
|
||||
---
|
||||
|
||||
*Managed by [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform). See [docs/workflows/update-server.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/workflows/update-server.md) for the full specification.*
|
||||
@@ -1,122 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: {{REPO_NAME}}.Documentation
|
||||
INGROUP: MokoPlatform.Templates
|
||||
REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: {{standards_version}}
|
||||
BRIEF: How this extension's Joomla update server file (updates.xml) is managed
|
||||
-->
|
||||
|
||||
# Joomla Update Server
|
||||
|
||||
[](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)
|
||||
|
||||
This document explains how `updates.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server).
|
||||
|
||||
## How It Works
|
||||
|
||||
Joomla checks for extension updates by fetching an XML file from the URL defined in the `<updateservers>` tag in the extension's XML manifest. moko-platform generates this file automatically.
|
||||
|
||||
### Automatic Generation
|
||||
|
||||
| Event | Workflow | `<tag>` | `<version>` |
|
||||
|-------|----------|---------|-------------|
|
||||
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
|
||||
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` |
|
||||
| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` |
|
||||
|
||||
### Generated XML Structure
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<updates>
|
||||
<update>
|
||||
<name>Extension Name</name>
|
||||
<description>Extension Name update</description>
|
||||
<element>com_extensionname</element>
|
||||
<type>component</type>
|
||||
<version>01.02.03</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder> <!-- plugins only -->
|
||||
<tags>
|
||||
<tag>stable</tag>
|
||||
</tags>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/.../releases/download/v01.02.03/com_ext-01.02.03.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="((5\.[0-9])|(6\.[0-9]))" />
|
||||
<php_minimum>8.2</php_minimum> <!-- if present in manifest -->
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
</updates>
|
||||
```
|
||||
|
||||
### Metadata Source
|
||||
|
||||
All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time:
|
||||
|
||||
| XML Element | Source | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `<name>` | `<name>` in manifest | Extension display name |
|
||||
| `<element>` | `<element>` in manifest | Must match installed extension identifier |
|
||||
| `<type>` | `type` attribute on `<extension>` | `component`, `module`, `plugin`, `library`, `package`, `template` |
|
||||
| `<client>` | `client` attribute on `<extension>` | `site` or `administrator` — **required for plugins and modules** |
|
||||
| `<folder>` | `group` attribute on `<extension>` | Plugin group (e.g., `system`, `content`) — **required for plugins** |
|
||||
| `<targetplatform>` | `<targetplatform>` in manifest | Falls back to Joomla 5.x / 6.x if not specified |
|
||||
| `<php_minimum>` | `<php_minimum>` in manifest | Included only if present |
|
||||
|
||||
### Extension Manifest Setup
|
||||
|
||||
Your XML manifest must include an `<updateservers>` tag pointing to the `updates.xml` on the `main` branch:
|
||||
|
||||
```xml
|
||||
<extension type="component" client="site" method="upgrade">
|
||||
<name>My Extension</name>
|
||||
<element>com_myextension</element>
|
||||
<!-- ... -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="My Extension Update Server (Gitea)">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="My Extension Update Server (GitHub)">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
```
|
||||
|
||||
### Branch Lifecycle
|
||||
|
||||
```
|
||||
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX
|
||||
(development) (rc) (stable) (frozen snapshot)
|
||||
```
|
||||
|
||||
1. **Development** (`dev/**`): `updates.xml` with `<tag>development</tag>`, download points to branch archive
|
||||
2. **Release Candidate** (`rc/**`): `updates.xml` with `<tag>rc</tag>`, version set to `XX.YY.ZZ-rc`
|
||||
3. **Stable Release** (merge to `main`): `updates.xml` with `<tag>stable</tag>`, download points to Gitea Release asset (primary) + GitHub Release asset (mirror)
|
||||
4. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed
|
||||
|
||||
### Health Checks
|
||||
|
||||
The `repo_health.yml` workflow verifies on every commit:
|
||||
|
||||
- `updates.xml` exists in the repository root
|
||||
- XML manifest exists with `<extension>` tag
|
||||
- `<version>`, `<name>`, `<author>`, `<namespace>` tags present
|
||||
- Extension `type` attribute is valid
|
||||
- Language `.ini` files exist
|
||||
- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/`
|
||||
|
||||
---
|
||||
|
||||
*Managed by [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform). See [docs/workflows/update-server.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/workflows/update-server.md) for the full specification.*
|
||||
@@ -1,119 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: {{VERSION}}
|
||||
-->
|
||||
|
||||
<updates>
|
||||
|
||||
<!-- 1. DEVELOPMENT -->
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>{{EXTENSION_NAME}} development build — unstable.</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<folder>{{EXTENSION_FOLDER}}</folder>
|
||||
<client>{{EXTENSION_CLIENT}}</client>
|
||||
<version>{{VERSION}}</version>
|
||||
<creationDate>{{DATE}}</creationDate>
|
||||
<infourl title='{{EXTENSION_NAME}} Dev'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/development</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/development/{{EXTENSION_ELEMENT}}-{{VERSION}}-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<tags><tag>development</tag></tags>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name='joomla' version='(5|6).*'/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
|
||||
<!-- 2. ALPHA -->
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>{{EXTENSION_NAME}} alpha build — early testing.</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<folder>{{EXTENSION_FOLDER}}</folder>
|
||||
<client>{{EXTENSION_CLIENT}}</client>
|
||||
<version>{{VERSION}}</version>
|
||||
<creationDate>{{DATE}}</creationDate>
|
||||
<infourl title='{{EXTENSION_NAME}} Alpha'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/alpha</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/alpha/{{EXTENSION_ELEMENT}}-{{VERSION}}-alpha.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name='joomla' version='(5|6).*'/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
|
||||
<!-- 3. BETA -->
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>{{EXTENSION_NAME}} beta build — feature complete, stability testing.</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<folder>{{EXTENSION_FOLDER}}</folder>
|
||||
<client>{{EXTENSION_CLIENT}}</client>
|
||||
<version>{{VERSION}}</version>
|
||||
<creationDate>{{DATE}}</creationDate>
|
||||
<infourl title='{{EXTENSION_NAME}} Beta'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/beta</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/beta/{{EXTENSION_ELEMENT}}-{{VERSION}}-beta.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<tags><tag>beta</tag></tags>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name='joomla' version='(5|6).*'/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
|
||||
<!-- 4. RC -->
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>{{EXTENSION_NAME}} release candidate — testing only.</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<folder>{{EXTENSION_FOLDER}}</folder>
|
||||
<client>{{EXTENSION_CLIENT}}</client>
|
||||
<version>{{VERSION}}</version>
|
||||
<creationDate>{{DATE}}</creationDate>
|
||||
<infourl title='{{EXTENSION_NAME}} RC'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/release-candidate</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/release-candidate/{{EXTENSION_ELEMENT}}-{{VERSION}}-rc.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name='joomla' version='(5|6).*'/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
|
||||
<!-- 5. STABLE -->
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>{{EXTENSION_NAME}} — Moko Consulting Joomla extension.</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<folder>{{EXTENSION_FOLDER}}</folder>
|
||||
<client>{{EXTENSION_CLIENT}}</client>
|
||||
<version>{{VERSION}}</version>
|
||||
<creationDate>{{DATE}}</creationDate>
|
||||
<infourl title='{{EXTENSION_NAME}}'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/stable/{{EXTENSION_ELEMENT}}-{{VERSION}}.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name='joomla' version='(5|6).*'/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
|
||||
</updates>
|
||||
@@ -1,366 +1,34 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoPlatform.Templates.GitHub
|
||||
INGROUP: MokoPlatform.Templates
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
PATH: /templates/mokogitea/CLAUDE.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: Standard CLAUDE.md template for Moko Consulting governed repositories
|
||||
NOTE: Synced to .gitea/CLAUDE.md in all governed repositories via bulk sync.
|
||||
Tokens replaced at sync time: {{REPO_NAME}}, {{REPO_URL}}, {{PRIMARY_LANGUAGE}}, {{PLATFORM_TYPE}}, {{REPO_DESCRIPTION}}
|
||||
-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **🔧 AI Self-Update Required on First Use**
|
||||
>
|
||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The moko-platform bulk-sync system
|
||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
||||
> this repository — or this is a brand-new repo.
|
||||
>
|
||||
> **On your very first interaction in this repository, before answering any other question:**
|
||||
>
|
||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
||||
> in **both** `.gitea/copilot-instructions.md` and `.gitea/CLAUDE.md`.
|
||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
||||
> through the closing `> ---` line) in both files.
|
||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
||||
> values — all set."*
|
||||
>
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{REPO_DESCRIPTION}}` | First paragraph of `README.md` body, or the GitHub repo description |
|
||||
> | `{{PRIMARY_LANGUAGE}}` | The dominant programming language (check file extensions in the repository) |
|
||||
> | `{{PLATFORM_TYPE}}` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure |
|
||||
>
|
||||
> ---
|
||||
|
||||
# What This Repo Is
|
||||
|
||||
**{{REPO_NAME}}** is a Moko Consulting **{{PLATFORM_TYPE}}** repository.
|
||||
# {{REPO_NAME}}
|
||||
|
||||
{{REPO_DESCRIPTION}}
|
||||
|
||||
Repository URL: {{REPO_URL}}
|
||||
## Quick Reference
|
||||
|
||||
This repository is governed by [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Platform** | {{PLATFORM_TYPE}} |
|
||||
| **Language** | {{PRIMARY_LANGUAGE}} |
|
||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||
| **Wiki** | [{{REPO_NAME}} Wiki](https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/wiki) |
|
||||
|
||||
---
|
||||
|
||||
# Repo Structure
|
||||
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── src/ # Primary source code
|
||||
├── docs/ # Documentation
|
||||
├── tests/ # Test suite
|
||||
├── .gitea/
|
||||
│ ├── workflows/ # CI/CD workflows (synced from moko-platform)
|
||||
│ ├── ISSUE_TEMPLATE/ # Issue templates (synced from moko-platform)
|
||||
│ ├── copilot-instructions.md # GitHub Copilot custom instructions
|
||||
│ ├── CLAUDE.md # This file — Claude AI assistant context
|
||||
│ └── override.tf # Repository-specific health-check overrides
|
||||
├── README.md # Project overview — version source of truth
|
||||
├── CHANGELOG.md # Version history
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
└── LICENSE # GPL-3.0-or-later
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Primary Language
|
||||
|
||||
**{{PRIMARY_LANGUAGE}}** is the primary language for this repository.
|
||||
|
||||
YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`.
|
||||
|
||||
---
|
||||
|
||||
# Composer Package (PHP repositories)
|
||||
|
||||
This repository requires the moko-platform enterprise library. The package is installed from the private GitHub VCS source.
|
||||
|
||||
`composer.json` must contain:
|
||||
|
||||
```json
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://git.mokoconsulting.tech/MokoConsulting/moko-platform"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"mokoconsulting/mokostandards": "^4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Install or update with:
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
composer install # first time
|
||||
composer update mokoconsulting/mokostandards # upgrade
|
||||
make build # Build the project
|
||||
make lint # Run linters
|
||||
make validate # Validate structure
|
||||
make release # Full release pipeline
|
||||
make clean # Clean build artifacts
|
||||
```
|
||||
|
||||
---
|
||||
## Architecture
|
||||
|
||||
# PHP Script Pattern
|
||||
<!-- Platform-specific: update this section for each repo -->
|
||||
|
||||
All PHP scripts must extend `MokoStandards\Enterprise\CliFramework` — **never** use a standalone class or the legacy `CliBase`.
|
||||
## Rules
|
||||
|
||||
```php
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: {{REPO_NAME}}.Scripts
|
||||
* INGROUP: {{REPO_NAME}}
|
||||
* REPO: {{REPO_URL}}
|
||||
* PATH: /api/my_script.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of what this script does
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use MokoStandards\Enterprise\CliFramework;
|
||||
|
||||
class MyScript extends CliFramework
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('One-line description of what this script does');
|
||||
$this->addArgument('--path', 'Repository root path', '.');
|
||||
$this->addArgument('--dry-run', 'Preview changes without writing', false);
|
||||
}
|
||||
|
||||
protected function run(): int
|
||||
{
|
||||
$path = $this->getArgument('--path');
|
||||
$dryRun = (bool) $this->getArgument('--dry-run');
|
||||
|
||||
// implementation …
|
||||
$this->log('INFO', "Processing: {$path}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
$script = new MyScript('my_script', 'One-line description of what this script does');
|
||||
exit($script->execute());
|
||||
```
|
||||
|
||||
**CliFramework interface summary:**
|
||||
|
||||
| Member | Purpose |
|
||||
|--------|---------|
|
||||
| `configure(): void` | Abstract — register arguments with `addArgument()` |
|
||||
| `run(): int` | Abstract — main script logic; return the exit code |
|
||||
| `initialize(): void` | Optional hook — runs after arg-parse, before `run()` |
|
||||
| `execute(array $argv = []): int` | **Public entry point** — call this at the bottom; it calls `configure()`, parses argv, then calls `run()` |
|
||||
| `addArgument(string $name, string $desc, mixed $default)` | Register a CLI argument |
|
||||
| `getArgument(string $name): mixed` | Read a parsed or default argument value |
|
||||
| `log(string $level, string $message)` | Structured log — levels: INFO SUCCESS WARNING ERROR DEBUG |
|
||||
| `error(string $message, int $code = 1): never` | Log error and exit |
|
||||
| `$this->dryRun` | `true` when `--dry-run` is passed |
|
||||
| `$this->verbose` | `true` when `--verbose` / `-v` is passed |
|
||||
|
||||
**Forbidden patterns in PHP:**
|
||||
|
||||
```php
|
||||
// ❌ Wrong — legacy base class, not namespaced
|
||||
class MyScript extends CliBase { … }
|
||||
|
||||
// ❌ Wrong — standalone class with no framework
|
||||
class MyScript { public function run() { … } }
|
||||
|
||||
// ❌ Wrong — method names and entry-point transposed
|
||||
protected function execute(): int { … } // should be run()
|
||||
exit($script->run()); // should be execute()
|
||||
|
||||
// ✅ Correct
|
||||
class MyScript extends CliFramework {
|
||||
protected function configure(): void { … }
|
||||
protected function run(): int { … }
|
||||
}
|
||||
$script = new MyScript('name', 'description');
|
||||
exit($script->execute());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Version Management
|
||||
|
||||
**`README.md` is the single source of truth for the repository version.**
|
||||
|
||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all badges and `FILE INFORMATION` headers automatically on merge to `main`.
|
||||
- The `VERSION: XX.YY.ZZ` field in the `README.md` `FILE INFORMATION` block governs all other version references.
|
||||
- Update `README.md` only — the `sync-version-on-merge` workflow propagates it to all badges and `FILE INFORMATION` headers automatically on merge to `main`.
|
||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
||||
- Never hardcode a version number in body text — use the badge or FILE INFORMATION header only.
|
||||
|
||||
---
|
||||
|
||||
# File Header Requirements
|
||||
|
||||
Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt.
|
||||
|
||||
## Minimal header
|
||||
|
||||
**PHP:**
|
||||
```php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: {{REPO_NAME}}.Module
|
||||
* INGROUP: {{REPO_NAME}}
|
||||
* REPO: {{REPO_URL}}
|
||||
* PATH: /src/MyClass.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of file purpose
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
```
|
||||
|
||||
**Markdown:**
|
||||
```markdown
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: {{REPO_NAME}}.Documentation
|
||||
INGROUP: {{REPO_NAME}}
|
||||
REPO: {{REPO_URL}}
|
||||
PATH: /docs/guide/example.md
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: One-line description of file purpose
|
||||
-->
|
||||
```
|
||||
|
||||
**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt.
|
||||
|
||||
---
|
||||
|
||||
# Coding Standards
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| PHP class | `PascalCase` | `MyService` |
|
||||
| PHP method / function | `camelCase` | `getUserData()` |
|
||||
| PHP variable | `$snake_case` | `$user_id` |
|
||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_RETRIES` |
|
||||
| PHP class file | `PascalCase.php` | `UserService.php` |
|
||||
| PHP script file | `snake_case.php` | `check_health.php` |
|
||||
| YAML workflow | `kebab-case.yml` | `code-quality.yml` |
|
||||
| Markdown doc | `kebab-case.md` | `coding-style-guide.md` |
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
||||
|
||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
||||
|
||||
## Branch Naming
|
||||
|
||||
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
|
||||
|
||||
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
|
||||
|
||||
---
|
||||
|
||||
# GitHub Actions — Token Usage
|
||||
|
||||
Every workflow in this repository must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
|
||||
```yaml
|
||||
# ✅ Correct — always use GH_TOKEN
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ❌ Wrong — never use these
|
||||
token: ${{ github.token }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is a local-dev fallback only.
|
||||
|
||||
---
|
||||
|
||||
# Keeping Documentation Current
|
||||
|
||||
Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale.
|
||||
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class |
|
||||
| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent |
|
||||
| New or changed GitHub Actions workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| New or changed policy | Corresponding file under `docs/policy/` |
|
||||
| New library class or major feature | `CHANGELOG.md` entry under `Added` |
|
||||
| Bug fix | `CHANGELOG.md` entry under `Fixed` |
|
||||
| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge |
|
||||
|
||||
If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR.
|
||||
|
||||
---
|
||||
|
||||
# What NOT to Do
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** — always use `secrets.GH_TOKEN`.
|
||||
- **Never extend `CliBase` in PHP scripts** — extend `MokoStandards\Enterprise\CliFramework` instead.
|
||||
- **Never use `exit($script->run())`** — the correct entry point is `exit($script->execute())`.
|
||||
|
||||
---
|
||||
|
||||
# Key Policy Documents (moko-platform)
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [scripting-standards.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage |
|
||||
| [package-installation.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer |
|
||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||
- **Attribution**: `Authored-by: Moko Consulting`
|
||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||
- **Standards**: [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||
|
||||
@@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase
|
||||
{
|
||||
file_put_contents(
|
||||
"{$this->tmpDir}/README.md",
|
||||
"<!-- VERSION: 09.24.00 -->\nSome content\n"
|
||||
"<!-- VERSION: 09.25.00 -->\nSome content\n"
|
||||
);
|
||||
|
||||
$this->execute();
|
||||
|
||||
@@ -34,7 +34,7 @@ class VersionReadTest extends TestCase
|
||||
{
|
||||
file_put_contents(
|
||||
"{$this->tmpDir}/README.md",
|
||||
"# Test\n<!-- VERSION: 09.24.00 -->\n"
|
||||
"# Test\n<!-- VERSION: 09.25.00 -->\n"
|
||||
);
|
||||
|
||||
$this->assertSame('02.03.04', trim($this->runScript()));
|
||||
@@ -68,7 +68,7 @@ class VersionReadTest extends TestCase
|
||||
{
|
||||
file_put_contents(
|
||||
"{$this->tmpDir}/README.md",
|
||||
"<!-- VERSION: 09.24.00 -->\n"
|
||||
"<!-- VERSION: 09.25.00 -->\n"
|
||||
);
|
||||
mkdir("{$this->tmpDir}/src", 0755, true);
|
||||
file_put_contents(
|
||||
|
||||
@@ -27,7 +27,8 @@ use MokoEnterprise\{
|
||||
PluginFactory,
|
||||
PluginRegistry,
|
||||
AuditLogger,
|
||||
MetricsCollector
|
||||
MetricsCollector,
|
||||
SourceResolver
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -228,8 +229,9 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy: site structure inside src/
|
||||
$siteDirs = ['src/administrator', 'src/components', 'src/plugins', 'src/templates', 'src/media'];
|
||||
// Legacy: site structure inside source/ or src/
|
||||
$srcName = SourceResolver::resolve($repoPath);
|
||||
$siteDirs = ["{$srcName}/administrator", "{$srcName}/components", "{$srcName}/plugins", "{$srcName}/templates", "{$srcName}/media"];
|
||||
$siteDirCount = 0;
|
||||
foreach ($siteDirs as $dir) {
|
||||
if (is_dir($repoPath . '/' . $dir)) {
|
||||
@@ -238,7 +240,7 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
if ($siteDirCount >= 3) {
|
||||
$score += 20;
|
||||
$indicators[] = "Joomla site structure in src/ ({$siteDirCount}/5 dirs)";
|
||||
$indicators[] = "Joomla site structure in {$srcName}/ ({$siteDirCount}/5 dirs)";
|
||||
}
|
||||
|
||||
// Negative: if there's a Joomla extension manifest (not type="file"), it's an extension
|
||||
@@ -710,17 +712,19 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
|
||||
// Check for MCP server entry point with McpServer usage
|
||||
if (file_exists("{$repoPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$repoPath}/src/index.ts");
|
||||
$mcpEntry = SourceResolver::findUnderSource($repoPath, 'index.ts');
|
||||
if ($mcpEntry !== null) {
|
||||
$content = @file_get_contents($mcpEntry);
|
||||
$mcpSrcName = SourceResolver::resolve($repoPath);
|
||||
if ($content) {
|
||||
if (strpos($content, 'McpServer') !== false) {
|
||||
$score += 0.3;
|
||||
$indicators[] = "Found McpServer import in src/index.ts";
|
||||
$indicators[] = "Found McpServer import in {$mcpSrcName}/index.ts";
|
||||
}
|
||||
if (strpos($content, 'server.tool(') !== false) {
|
||||
$score += 0.1;
|
||||
$toolCount = substr_count($content, 'server.tool(');
|
||||
$indicators[] = "Found {$toolCount} tool registrations in src/index.ts";
|
||||
$indicators[] = "Found {$toolCount} tool registrations in {$mcpSrcName}/index.ts";
|
||||
}
|
||||
if (strpos($content, 'StdioServerTransport') !== false) {
|
||||
$score += 0.1;
|
||||
@@ -730,16 +734,17 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
|
||||
// Check for the standard 4-file MCP structure
|
||||
$mcpFiles = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
||||
$mcpRequired = ['index.ts', 'client.ts', 'config.ts', 'types.ts'];
|
||||
$foundCount = 0;
|
||||
foreach ($mcpFiles as $file) {
|
||||
if (file_exists("{$repoPath}/{$file}")) {
|
||||
foreach ($mcpRequired as $file) {
|
||||
if (SourceResolver::findUnderSource($repoPath, $file) !== null) {
|
||||
$foundCount++;
|
||||
}
|
||||
}
|
||||
if ($foundCount === 4) {
|
||||
$score += 0.1;
|
||||
$indicators[] = "Found standard MCP 4-file src/ structure";
|
||||
$mcpSrcName = $mcpSrcName ?? SourceResolver::resolve($repoPath);
|
||||
$indicators[] = "Found standard MCP 4-file {$mcpSrcName}/ structure";
|
||||
}
|
||||
|
||||
// Check for setup wizard
|
||||
|
||||
@@ -30,7 +30,7 @@ use MokoEnterprise\CliFramework;
|
||||
class CheckChangelog extends CliFramework
|
||||
{
|
||||
/** Directories searched for CHANGELOG.md, relative to --path (case-insensitive match). */
|
||||
private const SEARCH_DIRS = ['', 'src', 'docs'];
|
||||
private const SEARCH_DIRS = ['', 'source', 'src', 'docs'];
|
||||
|
||||
/**
|
||||
* Configure available arguments.
|
||||
@@ -57,7 +57,7 @@ class CheckChangelog extends CliFramework
|
||||
$found = $this->findChangelog($path);
|
||||
|
||||
if ($found === null) {
|
||||
$this->status(false, 'CHANGELOG.md found (checked root, src/, docs/)');
|
||||
$this->status(false, 'CHANGELOG.md found (checked root, source/, src/, docs/)');
|
||||
$this->printSummary(0, 1, $this->elapsed());
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
/**
|
||||
* Validates client theme packages that deliver CSS, JS, and images
|
||||
@@ -44,17 +44,17 @@ class CheckClientTheme extends CliFramework
|
||||
/** Recommended XML elements. */
|
||||
private const RECOMMENDED_ELEMENTS = ['updateservers', 'scriptfile', 'description', 'fileset'];
|
||||
|
||||
/** Required theme CSS files relative to repo root. */
|
||||
/** Required theme CSS files relative to the source directory. */
|
||||
private const REQUIRED_THEME_FILES = [
|
||||
'src/media/templates/site/mokoonyx/css/theme/light.custom.css',
|
||||
'src/media/templates/site/mokoonyx/css/theme/dark.custom.css',
|
||||
'media/templates/site/mokoonyx/css/theme/light.custom.css',
|
||||
'media/templates/site/mokoonyx/css/theme/dark.custom.css',
|
||||
];
|
||||
|
||||
/** Optional but expected files. */
|
||||
/** Optional but expected files (paths prefixed with ~ are relative to source dir). */
|
||||
private const EXPECTED_FILES = [
|
||||
'src/media/templates/site/mokoonyx/css/user.css',
|
||||
'src/media/templates/site/mokoonyx/js/user.js',
|
||||
'src/script.php',
|
||||
'~media/templates/site/mokoonyx/css/user.css',
|
||||
'~media/templates/site/mokoonyx/js/user.js',
|
||||
'~script.php',
|
||||
'updates.xml',
|
||||
];
|
||||
|
||||
@@ -81,10 +81,12 @@ class CheckClientTheme extends CliFramework
|
||||
|
||||
// ── Manifest ──────────────────────────────────────────
|
||||
$this->section('Manifest validation');
|
||||
$manifest = $path . '/src/templateDetails.xml';
|
||||
$srcName = SourceResolver::resolve($path);
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
$manifest = $path . "/{$srcName}/templateDetails.xml";
|
||||
|
||||
if (!is_file($manifest)) {
|
||||
$this->status(false, 'Missing src/templateDetails.xml');
|
||||
$this->status(false, "Missing {$srcName}/templateDetails.xml");
|
||||
$this->printSummary(0, 1, $this->elapsed());
|
||||
return 1;
|
||||
}
|
||||
@@ -144,28 +146,36 @@ class CheckClientTheme extends CliFramework
|
||||
// ── Required files ────────────────────────────────────
|
||||
$this->section('Required files');
|
||||
foreach (self::REQUIRED_THEME_FILES as $file) {
|
||||
$full = $path . '/' . $file;
|
||||
$full = "{$path}/{$srcName}/{$file}";
|
||||
if (is_file($full)) {
|
||||
$this->status(true, basename($file));
|
||||
} else {
|
||||
$this->status(false, "Missing: {$file}");
|
||||
$this->status(false, "Missing: {$srcName}/{$file}");
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::EXPECTED_FILES as $file) {
|
||||
$full = $path . '/' . $file;
|
||||
// Paths prefixed with ~ are relative to source dir
|
||||
if (str_starts_with($file, '~')) {
|
||||
$relFile = substr($file, 1);
|
||||
$full = "{$path}/{$srcName}/{$relFile}";
|
||||
$display = "{$srcName}/{$relFile}";
|
||||
} else {
|
||||
$full = "{$path}/{$file}";
|
||||
$display = $file;
|
||||
}
|
||||
if (is_file($full)) {
|
||||
$this->status(true, basename($file));
|
||||
} else {
|
||||
$this->warning("Missing: {$file}");
|
||||
$this->warning("Missing: {$display}");
|
||||
$warns++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── PHP syntax ────────────────────────────────────────
|
||||
$this->section('PHP syntax');
|
||||
$phpFiles = glob($path . '/src/*.php') ?: [];
|
||||
$phpFiles = glob("{$path}/{$srcName}/*.php") ?: [];
|
||||
foreach ($phpFiles as $phpFile) {
|
||||
$output = [];
|
||||
$ret = 0;
|
||||
@@ -179,20 +189,20 @@ class CheckClientTheme extends CliFramework
|
||||
}
|
||||
}
|
||||
if (empty($phpFiles)) {
|
||||
$this->warning('No PHP files in src/');
|
||||
$this->warning("No PHP files in {$srcName}/");
|
||||
}
|
||||
|
||||
// ── CSS validation ────────────────────────────────────
|
||||
$this->section('CSS validation');
|
||||
$cssFiles = array_merge(
|
||||
glob($path . '/src/media/templates/site/mokoonyx/css/theme/*.css') ?: [],
|
||||
glob($path . '/src/media/templates/site/mokoonyx/css/*.css') ?: [],
|
||||
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/theme/*.css") ?: [],
|
||||
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/*.css") ?: [],
|
||||
);
|
||||
foreach ($cssFiles as $cssFile) {
|
||||
$css = (string) file_get_contents($cssFile);
|
||||
$open = substr_count($css, '{');
|
||||
$close = substr_count($css, '}');
|
||||
$name = str_replace($path . '/src/', '', $cssFile);
|
||||
$name = str_replace("{$path}/{$srcName}/", '', $cssFile);
|
||||
|
||||
if ($open !== $close) {
|
||||
$this->status(false, "Unbalanced braces in {$name} (open: {$open}, close: {$close})");
|
||||
@@ -241,7 +251,7 @@ class CheckClientTheme extends CliFramework
|
||||
// ── Image sizes ───────────────────────────────────────
|
||||
$this->section('Image optimization');
|
||||
$largeImages = 0;
|
||||
$imageDir = $path . '/src/images';
|
||||
$imageDir = "{$path}/{$srcName}/images";
|
||||
if (is_dir($imageDir)) {
|
||||
$iter = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($imageDir, \FilesystemIterator::SKIP_DOTS)
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
/**
|
||||
* Validates the required directory structure of a Dolibarr module repository.
|
||||
@@ -47,33 +47,36 @@ class CheckDolibarrModule extends CliFramework
|
||||
$failed = 0;
|
||||
|
||||
$this->section('Checking directory structure');
|
||||
$srcName = SourceResolver::resolve($path);
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
|
||||
if (!is_dir($path . '/src')) {
|
||||
$this->status(false, 'src/ directory exists');
|
||||
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||
if ($srcDir === null) {
|
||||
$this->status(false, 'source/ or src/ directory exists');
|
||||
$failed++;
|
||||
} else {
|
||||
$this->status(true, 'src/ directory exists');
|
||||
$this->status(true, "{$srcName}/ directory exists");
|
||||
$passed++;
|
||||
}
|
||||
|
||||
if (!is_dir($path . '/src/core/modules')) {
|
||||
$this->status(false, 'src/core/modules/ directory exists');
|
||||
if (!is_dir($path . "/{$srcName}/core/modules")) {
|
||||
$this->status(false, "{$srcName}/core/modules/ directory exists");
|
||||
$failed++;
|
||||
} else {
|
||||
$this->status(true, 'src/core/modules/ directory exists');
|
||||
$this->status(true, "{$srcName}/core/modules/ directory exists");
|
||||
$passed++;
|
||||
}
|
||||
|
||||
if (!is_dir($path . '/src/langs')) {
|
||||
$this->warning('Missing suggested directory: src/langs/');
|
||||
if (!is_dir($path . "/{$srcName}/langs")) {
|
||||
$this->warning("Missing suggested directory: {$srcName}/langs/");
|
||||
} else {
|
||||
$this->status(true, 'src/langs/ directory exists');
|
||||
$this->status(true, "{$srcName}/langs/ directory exists");
|
||||
$passed++;
|
||||
}
|
||||
|
||||
$this->section('Checking module descriptor');
|
||||
|
||||
$descriptors = glob($path . '/src/core/modules/mod*.class.php') ?: [];
|
||||
$descriptors = glob($path . "/{$srcName}/core/modules/mod*.class.php") ?: [];
|
||||
if (empty($descriptors)) {
|
||||
$this->status(false, 'Module descriptor found (mod*.class.php)');
|
||||
$failed++;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /validate/check_file_integrity.php
|
||||
* VERSION: 09.24.00
|
||||
* VERSION: 09.25.00
|
||||
* BRIEF: Compare deployed files on a remote server against the local repository to detect drift
|
||||
*/
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class CheckStructure extends CliFramework
|
||||
private const REQUIRED_FILES = ['README.md', 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md'];
|
||||
|
||||
/** Directories searched for CHANGELOG.md (case-insensitive), relative to repo root. */
|
||||
private const CHANGELOG_DIRS = ['', 'src', 'docs'];
|
||||
private const CHANGELOG_DIRS = ['', 'source', 'src', 'docs'];
|
||||
|
||||
/**
|
||||
* Configure available arguments.
|
||||
|
||||
Reference in New Issue
Block a user