Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab05bb7008 | |||
| 6bd26698c4 | |||
| 19b504526b | |||
| e7bdf7cbc7 | |||
| ff5794d0cc | |||
| bfba45e8b5 | |||
| 78ea05233b | |||
| ae0d54310d | |||
| 9df59836bf | |||
| 6e40707223 | |||
| ca55e5d2d2 | |||
| 9526d006c4 | |||
| c90a5671bd | |||
| 048a7d71d1 | |||
| c57b5724ac | |||
| 78affd37ff | |||
| b3062c6559 | |||
| 9dab9f1ef6 | |||
| c61d32709c |
@@ -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)
|
||||||
@@ -45,12 +45,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if [ -f "/opt/moko-platform/cli/version_bump.php" ] && [ -f "/opt/moko-platform/vendor/autoload.php" ]; 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
|
echo "Using pre-installed /opt/moko-platform"
|
||||||
fi
|
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
else
|
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 \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||||
/tmp/moko-platform-api
|
/tmp/moko-platform-api
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | 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 |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
@@ -71,20 +71,25 @@ jobs:
|
|||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; 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
|
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
|
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
|
- name: Rename branch to rc
|
||||||
run: |
|
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 \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -100,16 +105,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
--skip-update-stream
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
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) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -151,25 +155,60 @@ jobs:
|
|||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
# Ensure PHP + Composer are available
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
if ! command -v composer &> /dev/null; then
|
echo Using pre-installed /opt/moko-platform
|
||||||
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
|
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
|
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"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
--skip-update-stream
|
|
||||||
|
- 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) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
@@ -182,7 +221,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
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" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -256,7 +295,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
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}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/ci-platform.yml
|
# PATH: /.mokogitea/workflows/ci-platform.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: moko-platform CI — the standards engine validates itself
|
# BRIEF: moko-platform CI — the standards engine validates itself
|
||||||
#
|
#
|
||||||
@@ -41,7 +41,7 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'wiki/**'
|
- 'wiki/**'
|
||||||
- '.gitea/ISSUE_TEMPLATE/**'
|
- '.mokogitea/ISSUE_TEMPLATE/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
echo "::error file=${file}::PHP syntax error"
|
echo "::error file=${file}::PHP syntax error"
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
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"
|
echo "### PHP Syntax"
|
||||||
@@ -270,7 +270,7 @@ jobs:
|
|||||||
echo "::warning file=${file}::Missing SPDX header"
|
echo "::warning file=${file}::Missing SPDX header"
|
||||||
MISSING=$((MISSING + 1))
|
MISSING=$((MISSING + 1))
|
||||||
fi
|
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"
|
echo "### License Headers"
|
||||||
@@ -289,7 +289,7 @@ jobs:
|
|||||||
echo "::error file=${file}::Potential hardcoded secret detected"
|
echo "::error file=${file}::Potential hardcoded secret detected"
|
||||||
FOUND=$((FOUND + 1))
|
FOUND=$((FOUND + 1))
|
||||||
fi
|
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"
|
echo "### Secret Detection"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: moko-platform.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.mokogitea/workflows/notify.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ jobs:
|
|||||||
if: steps.platform.outputs.platform == 'joomla'
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
MISSING=0
|
MISSING=0
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="source"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
while IFS= read -r dir; do
|
while IFS= read -r dir; do
|
||||||
if [ ! -f "${dir}/index.html" ]; then
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
@@ -220,7 +221,7 @@ jobs:
|
|||||||
echo "joomla.asset.json: valid"
|
echo "joomla.asset.json: valid"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate all XML files in src/ are well-formed
|
# Validate all XML files in source/src/ are well-formed
|
||||||
XML_ERRORS=0
|
XML_ERRORS=0
|
||||||
if command -v php &> /dev/null; then
|
if command -v php &> /dev/null; then
|
||||||
while IFS= read -r -d '' xmlfile; do
|
while IFS= read -r -d '' xmlfile; do
|
||||||
@@ -451,10 +452,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify package source
|
- name: Verify package source
|
||||||
run: |
|
run: |
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="source"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
echo "::warning::No src/ or htdocs/ directory"
|
echo "::warning::No source/, src/, or htdocs/ directory"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# 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
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
@@ -17,6 +17,10 @@ on:
|
|||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
pull_request_target:
|
||||||
|
types: [synchronize, opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -43,7 +47,8 @@ jobs:
|
|||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -51,22 +56,28 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||||
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/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
|
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
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
@@ -76,31 +87,43 @@ jobs:
|
|||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
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
|
case "$STABILITY" in
|
||||||
development) TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
alpha) TAG="alpha" ;;
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
beta) TAG="beta" ;;
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
release-candidate) TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
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
|
case "$STABILITY" in
|
||||||
release-candidate) php ${MOKO_CLI}/version_bump.php --path . --minor 2>/dev/null || true ;;
|
release-candidate) BUMP="minor" ;;
|
||||||
*) php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true ;;
|
*) BUMP="patch" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Set stability suffix and fix consistency
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')
|
|
||||||
|
# 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\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
--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
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
# Read final version with suffix
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
|
||||||
|
# Append suffix for output
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
@@ -125,11 +148,12 @@ jobs:
|
|||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$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
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
@@ -142,6 +166,41 @@ jobs:
|
|||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
--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
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -296,17 +296,19 @@ jobs:
|
|||||||
missing_required=()
|
missing_required=()
|
||||||
missing_optional=()
|
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=""
|
SOURCE_DIR=""
|
||||||
if [ -d "src" ]; then
|
if [ -d "source" ]; then
|
||||||
|
SOURCE_DIR="source"
|
||||||
|
elif [ -d "src" ]; then
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
elif [ -d "htdocs" ]; then
|
elif [ -d "htdocs" ]; then
|
||||||
SOURCE_DIR="htdocs"
|
SOURCE_DIR="htdocs"
|
||||||
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||||
# Platform/tooling repos don't need src/
|
# Platform/tooling repos don't need source/
|
||||||
SOURCE_DIR=""
|
SOURCE_DIR=""
|
||||||
else
|
else
|
||||||
missing_required+=("src/ or htdocs/ (source directory required)")
|
missing_required+=("source/ or src/ or htdocs/ (source directory required)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for item in "${required_artifacts[@]}"; do
|
for item in "${required_artifacts[@]}"; do
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.mokogitea/workflows/security-audit.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# 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,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
|
|
||||||
@@ -31,7 +31,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
use phpseclib3\Net\SFTP;
|
use phpseclib3\Net\SFTP;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
||||||
|
|
||||||
@@ -866,11 +866,11 @@ class DeployJoomla extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3-5. Fallback chain
|
// 3-5. Fallback chain (source/ → src/ → htdocs/)
|
||||||
foreach (['src', 'htdocs'] as $candidate) {
|
$resolved = SourceResolver::resolveAbsolute($repoPath);
|
||||||
if (is_dir("{$repoPath}/{$candidate}")) {
|
if ($resolved !== null) {
|
||||||
return "{$repoPath}/{$candidate}";
|
SourceResolver::warnIfLegacy($repoPath);
|
||||||
}
|
return $resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: repo root itself
|
// Last resort: repo root itself
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class JoomlaBuildCli extends CliFramework
|
class JoomlaBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -49,17 +49,12 @@ class JoomlaBuildCli extends CliFramework
|
|||||||
$path = realpath($path) ?: $path;
|
$path = realpath($path) ?: $path;
|
||||||
|
|
||||||
// ── Find source directory ──────────────────────────────────────────────
|
// ── Find source directory ──────────────────────────────────────────────
|
||||||
$srcDir = null;
|
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||||
foreach (['src', 'htdocs'] as $d) {
|
|
||||||
if (is_dir("{$path}/{$d}")) {
|
|
||||||
$srcDir = "{$path}/{$d}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($srcDir === null) {
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($path);
|
||||||
|
|
||||||
// ── Find manifest ──────────────────────────────────────────────────────
|
// ── Find manifest ──────────────────────────────────────────────────────
|
||||||
$manifest = $this->findManifest($srcDir);
|
$manifest = $this->findManifest($srcDir);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
|
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Joomla Release Manager
|
* Joomla Release Manager
|
||||||
@@ -121,11 +121,12 @@ class JoomlaRelease extends CliFramework
|
|||||||
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
|
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
|
||||||
|
|
||||||
// ── Step 3: Build packages ────────────────────────────────────
|
// ── 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) {
|
if ($srcDir === null) {
|
||||||
$this->log('ERROR', 'No src/ or htdocs/ directory');
|
$this->log('ERROR', 'No source/ or src/ directory');
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($path);
|
||||||
|
|
||||||
$prefix = $this->typePrefix($meta);
|
$prefix = $this->typePrefix($meta);
|
||||||
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
|
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ManifestElementCli extends CliFramework
|
class ManifestElementCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ class ManifestElementCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$extManifest = null;
|
$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) {
|
foreach ($manifestFiles as $file) {
|
||||||
$c = file_get_contents($file);
|
$c = file_get_contents($file);
|
||||||
if (strpos($c, '<extension') !== false) {
|
if (strpos($c, '<extension') !== false) {
|
||||||
@@ -58,8 +58,7 @@ class ManifestElementCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$modFile = null;
|
$modFile = null;
|
||||||
$modFiles = array_merge(
|
$modFiles = array_merge(
|
||||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
);
|
);
|
||||||
foreach ($modFiles as $file) {
|
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());
|
||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class PackageBuildCli extends CliFramework
|
class PackageBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -56,18 +56,13 @@ class PackageBuildCli extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Determine source directory -----------------------------------------------
|
// -- Determine source directory -----------------------------------------------
|
||||||
$sourceDir = null;
|
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||||
foreach (['src', 'htdocs'] as $candidate) {
|
|
||||||
if (is_dir("{$root}/{$candidate}")) {
|
|
||||||
$sourceDir = "{$root}/{$candidate}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sourceDir === null) {
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
|
|
||||||
// -- Determine element and type prefix from manifest --------------------------
|
// -- Determine element and type prefix from manifest --------------------------
|
||||||
$extElement = $elementOverride;
|
$extElement = $elementOverride;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleaseCreateCli extends CliFramework
|
class ReleaseCreateCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -97,8 +97,8 @@ class ReleaseCreateCli extends CliFramework
|
|||||||
// Find extension manifest (Joomla XML)
|
// Find extension manifest (Joomla XML)
|
||||||
$extManifest = null;
|
$extManifest = null;
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($manifestFiles as $file) {
|
foreach ($manifestFiles as $file) {
|
||||||
@@ -112,8 +112,7 @@ class ReleaseCreateCli extends CliFramework
|
|||||||
// Find Dolibarr module file
|
// Find Dolibarr module file
|
||||||
$modFile = null;
|
$modFile = null;
|
||||||
$modFiles = array_merge(
|
$modFiles = array_merge(
|
||||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
);
|
);
|
||||||
foreach ($modFiles as $file) {
|
foreach ($modFiles as $file) {
|
||||||
|
|||||||
+15
-15
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleasePackageCli extends CliFramework
|
class ReleasePackageCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -99,9 +99,10 @@ class ReleasePackageCli extends CliFramework
|
|||||||
$extFolder = '';
|
$extFolder = '';
|
||||||
$typePrefix = '';
|
$typePrefix = '';
|
||||||
|
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -200,14 +201,12 @@ class ReleasePackageCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sourceDir === null && is_dir("{$root}/src")) {
|
if ($sourceDir === null) {
|
||||||
$sourceDir = "{$root}/src";
|
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||||
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
|
|
||||||
$sourceDir = "{$root}/htdocs";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sourceDir === null) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,19 +230,20 @@ class ReleasePackageCli extends CliFramework
|
|||||||
$subZipPath = "{$outputDir}/{$subName}.zip";
|
$subZipPath = "{$outputDir}/{$subName}.zip";
|
||||||
|
|
||||||
// If sub-package is a full repo checkout (e.g. git submodule),
|
// If sub-package is a full repo checkout (e.g. git submodule),
|
||||||
// look for a src/ subdirectory containing a Joomla manifest XML
|
// look for a source/ or src/ subdirectory containing a Joomla manifest XML
|
||||||
// and zip that instead of the repo root.
|
// and zip that instead of the repo root.
|
||||||
$subSourceDir = $pkgDir;
|
$subSourceDir = $pkgDir;
|
||||||
$srcCandidate = "{$pkgDir}/src";
|
$subSrcAbs = SourceResolver::resolveAbsolute($pkgDir);
|
||||||
if (is_dir($srcCandidate)) {
|
if ($subSrcAbs !== null) {
|
||||||
$srcManifests = array_merge(
|
$srcManifests = array_merge(
|
||||||
glob("{$srcCandidate}/*.xml") ?: [],
|
glob("{$subSrcAbs}/*.xml") ?: [],
|
||||||
glob("{$srcCandidate}/pkg_*.xml") ?: []
|
glob("{$subSrcAbs}/pkg_*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($srcManifests as $mf) {
|
foreach ($srcManifests as $mf) {
|
||||||
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
|
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
|
||||||
$subSourceDir = $srcCandidate;
|
$subSourceDir = $subSrcAbs;
|
||||||
echo " Sub-package {$subName}: using src/ entry-point\n";
|
$subSrcName = SourceResolver::resolve($pkgDir);
|
||||||
|
echo " Sub-package {$subName}: using {$subSrcName}/ entry-point\n";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleasePromoteCli extends CliFramework
|
class ReleasePromoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -109,8 +109,8 @@ class ReleasePromoteCli extends CliFramework
|
|||||||
if ($to === 'stable') {
|
if ($to === 'stable') {
|
||||||
$root = realpath($path) ?: $path;
|
$root = realpath($path) ?: $path;
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($manifestFiles as $xmlFile) {
|
foreach ($manifestFiles as $xmlFile) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleaseValidateCli extends CliFramework
|
class ReleaseValidateCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -66,8 +66,10 @@ class ReleaseValidateCli extends CliFramework
|
|||||||
$platform = 'generic';
|
$platform = 'generic';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$hasSource = is_dir("{$root}/src") || is_dir("{$root}/htdocs");
|
$hasSource = SourceResolver::resolveAbsolute($root) !== null;
|
||||||
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? 'src/ or htdocs/ found' : 'No src/ or htdocs/ directory');
|
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")) {
|
if (!file_exists("{$root}/README.md")) {
|
||||||
$this->addVResult('README.md', 'FAIL', 'Not found');
|
$this->addVResult('README.md', 'FAIL', 'Not found');
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +111,8 @@ class ReleaseValidateCli extends CliFramework
|
|||||||
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
|
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
|
||||||
if ($platform === 'joomla') {
|
if ($platform === 'joomla') {
|
||||||
$manifest = null;
|
$manifest = null;
|
||||||
foreach (["{$root}/src", $root] as $dir) {
|
$srcAbs = SourceResolver::resolveAbsolute($root);
|
||||||
|
foreach (array_filter([$srcAbs, $root]) as $dir) {
|
||||||
if (!is_dir($dir)) {
|
if (!is_dir($dir)) {
|
||||||
continue;
|
continue;
|
||||||
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
|
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
|
||||||
@@ -156,7 +159,7 @@ class ReleaseValidateCli extends CliFramework
|
|||||||
}
|
}
|
||||||
} elseif ($platform === 'dolibarr') {
|
} elseif ($platform === 'dolibarr') {
|
||||||
$modFile = null;
|
$modFile = null;
|
||||||
foreach (['src', 'htdocs'] as $sd) {
|
foreach (SourceResolver::getCandidates() as $sd) {
|
||||||
$matches = glob("{$root}/{$sd}/mod*.class.php");
|
$matches = glob("{$root}/{$sd}/mod*.class.php");
|
||||||
if (!empty($matches)) {
|
if (!empty($matches)) {
|
||||||
$modFile = $matches[0];
|
$modFile = $matches[0];
|
||||||
|
|||||||
+4
-9
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ThemeLintCli extends CliFramework
|
class ThemeLintCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -41,17 +41,12 @@ class ThemeLintCli extends CliFramework
|
|||||||
$errors = 0;
|
$errors = 0;
|
||||||
$warnings = 0;
|
$warnings = 0;
|
||||||
|
|
||||||
$srcDir = null;
|
$srcDir = SourceResolver::resolveAbsolute($root);
|
||||||
foreach (['src', 'htdocs'] as $d) {
|
|
||||||
if (is_dir("{$root}/{$d}")) {
|
|
||||||
$srcDir = "{$root}/{$d}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($srcDir === null) {
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
|
|
||||||
echo "Theme Lint: {$srcDir}\n\n";
|
echo "Theme Lint: {$srcDir}\n\n";
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class UpdatesXmlBuildCli extends CliFramework
|
class UpdatesXmlBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -109,7 +109,7 @@ class UpdatesXmlBuildCli extends CliFramework
|
|||||||
// -- Locate Joomla manifest ---------------------------------------------------
|
// -- Locate Joomla manifest ---------------------------------------------------
|
||||||
$manifest = null;
|
$manifest = null;
|
||||||
|
|
||||||
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
$candidates = SourceResolver::globSource($root, 'pkg_*.xml');
|
||||||
foreach ($candidates as $f) {
|
foreach ($candidates as $f) {
|
||||||
if (strpos(file_get_contents($f), '<extension') !== false) {
|
if (strpos(file_get_contents($f), '<extension') !== false) {
|
||||||
$manifest = $f;
|
$manifest = $f;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionBumpCli extends CliFramework
|
class VersionBumpCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -61,11 +61,12 @@ class VersionBumpCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$manifestVersion = null;
|
$manifestVersion = null;
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/src/packages/*/mokowaas.xml") ?: [],
|
SourceResolver::globSource($root, 'packages/*/mokowaas.xml'),
|
||||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($manifestFiles as $xmlFile) {
|
foreach ($manifestFiles as $xmlFile) {
|
||||||
@@ -141,7 +142,8 @@ class VersionBumpCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$updatedFiles = [];
|
$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) {
|
foreach (glob($pattern) ?: [] as $xmlFile) {
|
||||||
$content = file_get_contents($xmlFile);
|
$content = file_get_contents($xmlFile);
|
||||||
if (strpos($content, '<extension') === false) {
|
if (strpos($content, '<extension') === false) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionBumpRemoteCli extends CliFramework
|
class VersionBumpRemoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -104,11 +104,15 @@ class VersionBumpRemoteCli extends CliFramework
|
|||||||
$nextVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch);
|
$nextVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch);
|
||||||
echo "{$version} -> {$nextVersion} ({$branch})\n";
|
echo "{$version} -> {$nextVersion} ({$branch})\n";
|
||||||
|
|
||||||
|
// Try both source/ and src/ paths for backwards compatibility with remote repos
|
||||||
$manifestPaths = [];
|
$manifestPaths = [];
|
||||||
if ($manifestFile !== null) {
|
foreach (['source', 'src'] as $srcPrefix) {
|
||||||
$manifestPaths[] = "src/{$manifestFile}";
|
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;
|
$manifestUpdated = false;
|
||||||
foreach ($manifestPaths as $mPath) {
|
foreach ($manifestPaths as $mPath) {
|
||||||
$result = $this->updateRemoteFile($apiBase, $token, $mPath, $branch, function (string $content) use ($version, $nextVersion): string {
|
$result = $this->updateRemoteFile($apiBase, $token, $mPath, $branch, function (string $content) use ($version, $nextVersion): string {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionCheckCli extends CliFramework
|
class VersionCheckCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -77,7 +77,8 @@ class VersionCheckCli extends CliFramework
|
|||||||
$versions['pyproject.toml'] = $m[1];
|
$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) {
|
foreach (glob($glob) ?: [] as $file) {
|
||||||
if (basename($file) === 'updates.xml') {
|
if (basename($file) === 'updates.xml') {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionReadCli extends CliFramework
|
class VersionReadCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -64,9 +64,9 @@ class VersionReadCli extends CliFramework
|
|||||||
// -- 3. Fallback: Joomla manifest XML --
|
// -- 3. Fallback: Joomla manifest XML --
|
||||||
$manifestVersion = null;
|
$manifestVersion = null;
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionSetPlatformCli extends CliFramework
|
class VersionSetPlatformCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -110,7 +110,8 @@ class VersionSetPlatformCli extends CliFramework
|
|||||||
|
|
||||||
// Dolibarr: $this->version + $this->url_last_version in mod*.class.php
|
// Dolibarr: $this->version + $this->url_last_version in mod*.class.php
|
||||||
if ($platform === 'crm-module') {
|
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) {
|
foreach (glob($pattern) ?: [] as $file) {
|
||||||
$content = file_get_contents($file);
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
@@ -146,9 +147,10 @@ class VersionSetPlatformCli extends CliFramework
|
|||||||
|
|
||||||
// Joomla: <version> in XML manifests (top-level + sub-packages)
|
// Joomla: <version> in XML manifests (top-level + sub-packages)
|
||||||
if (in_array($platform, ['waas-component', 'joomla'], true)) {
|
if (in_array($platform, ['waas-component', 'joomla'], true)) {
|
||||||
|
$srcName = SourceResolver::resolve($root);
|
||||||
$xmlFiles = array_merge(
|
$xmlFiles = array_merge(
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
glob("{$root}/{$srcName}/*.xml") ?: [],
|
||||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
glob("{$root}/{$srcName}/packages/*/*.xml") ?: [],
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
if (empty($xmlFiles)) {
|
if (empty($xmlFiles)) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
use phpseclib3\Net\SFTP;
|
use phpseclib3\Net\SFTP;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
||||||
|
|
||||||
@@ -51,9 +51,9 @@ class DeploySftp extends CliFramework
|
|||||||
|
|
||||||
protected function configure(): void
|
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('--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('--env', 'Target environment: dev or rs', '');
|
||||||
$this->addArgument('--config', 'Explicit config file path — overrides --env', '');
|
$this->addArgument('--config', 'Explicit config file path — overrides --env', '');
|
||||||
$this->addArgument('--key-passphrase', 'Passphrase for the SSH private key', '');
|
$this->addArgument('--key-passphrase', 'Passphrase for the SSH private key', '');
|
||||||
@@ -158,7 +158,8 @@ class DeploySftp extends CliFramework
|
|||||||
*/
|
*/
|
||||||
private function resolveSrcDir(string $repoPath): string
|
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;
|
$dir = $repoPath . DIRECTORY_SEPARATOR . $sub;
|
||||||
|
|
||||||
if (!is_dir($dir)) {
|
if (!is_dir($dir)) {
|
||||||
|
|||||||
@@ -171,6 +171,13 @@ abstract class CliFramework
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $name = '', string $version = '04.00.15')
|
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->scriptName = $name ?: basename($_SERVER['argv'][0] ?? 'script', '.php');
|
||||||
$this->scriptVersion = $version;
|
$this->scriptVersion = $version;
|
||||||
$this->startTime = microtime(true);
|
$this->startTime = microtime(true);
|
||||||
|
|||||||
@@ -147,31 +147,29 @@ class ManifestReader
|
|||||||
/**
|
/**
|
||||||
* Get the source/entry-point directory.
|
* 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
|
* @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
|
public function getSourceDir(string $root = ''): string
|
||||||
{
|
{
|
||||||
$entryPoint = $this->get('entry-point', '');
|
$entryPoint = $this->get('entry-point', '');
|
||||||
if ($entryPoint !== '') {
|
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), '/');
|
$dir = rtrim(dirname($entryPoint) === '.' ? $entryPoint : dirname($entryPoint), '/');
|
||||||
if ($root === '' || is_dir("{$root}/{$dir}")) {
|
if ($root === '' || is_dir("{$root}/{$dir}")) {
|
||||||
return $dir;
|
return $dir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check common directories
|
// Fallback: use SourceResolver (source/ → src/ → htdocs/ → default 'source')
|
||||||
if ($root !== '') {
|
if ($root !== '') {
|
||||||
if (is_dir("{$root}/src")) {
|
return SourceResolver::resolve($root);
|
||||||
return 'src';
|
|
||||||
}
|
|
||||||
if (is_dir("{$root}/htdocs")) {
|
|
||||||
return 'htdocs';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'src';
|
return 'source';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class PackageBuilder
|
|||||||
mkdir($packageDir, 0755, true);
|
mkdir($packageDir, 0755, true);
|
||||||
mkdir($distDir, 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)) {
|
if (is_dir($repoRoot . '/' . $dir)) {
|
||||||
self::copyDirectory($repoRoot . '/' . $dir, $packageDir . '/' . $dir);
|
self::copyDirectory($repoRoot . '/' . $dir, $packageDir . '/' . $dir);
|
||||||
}
|
}
|
||||||
@@ -94,15 +95,15 @@ class PackageBuilder
|
|||||||
/**
|
/**
|
||||||
* Build a Dolibarr module release package.
|
* Build a Dolibarr module release package.
|
||||||
*
|
*
|
||||||
* Copies everything under src/ into a build staging directory and archives
|
* Copies everything under source/ (or src/) into a build staging directory
|
||||||
* it as dist/<MODULE_NAME>_<VERSION>.zip.
|
* and archives it as dist/<MODULE_NAME>_<VERSION>.zip.
|
||||||
*
|
*
|
||||||
* @param string $repoRoot Absolute path to the repository root.
|
* @param string $repoRoot Absolute path to the repository root.
|
||||||
* @param string $moduleName Module name (used in archive filename).
|
* @param string $moduleName Module name (used in archive filename).
|
||||||
* @param string $version Version string.
|
* @param string $version Version string.
|
||||||
* @param bool $dryRun When true, preview without writing.
|
* @param bool $dryRun When true, preview without writing.
|
||||||
* @return string Path to the created archive (or would-create path in dry-run).
|
* @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(
|
public static function buildDolibarr(
|
||||||
string $repoRoot,
|
string $repoRoot,
|
||||||
@@ -110,14 +111,15 @@ class PackageBuilder
|
|||||||
string $version,
|
string $version,
|
||||||
bool $dryRun = false
|
bool $dryRun = false
|
||||||
): string {
|
): string {
|
||||||
$srcDir = $repoRoot . '/src';
|
$srcDir = SourceResolver::resolveAbsolute($repoRoot);
|
||||||
$buildDir = $repoRoot . '/build';
|
$buildDir = $repoRoot . '/build';
|
||||||
$distDir = $repoRoot . '/dist';
|
$distDir = $repoRoot . '/dist';
|
||||||
$archivePath = $distDir . '/' . $moduleName . '_' . $version . '.zip';
|
$archivePath = $distDir . '/' . $moduleName . '_' . $version . '.zip';
|
||||||
|
|
||||||
if (!is_dir($srcDir)) {
|
if ($srcDir === null) {
|
||||||
throw new \RuntimeException("src/ directory not found at {$srcDir}");
|
throw new \RuntimeException("source/ or src/ directory not found in {$repoRoot}");
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($repoRoot);
|
||||||
|
|
||||||
if ($dryRun) {
|
if ($dryRun) {
|
||||||
return $archivePath;
|
return $archivePath;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ declare(strict_types=1);
|
|||||||
namespace MokoEnterprise\Plugins;
|
namespace MokoEnterprise\Plugins;
|
||||||
|
|
||||||
use MokoEnterprise\AbstractProjectPlugin;
|
use MokoEnterprise\AbstractProjectPlugin;
|
||||||
|
use MokoEnterprise\SourceResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP Server Project Plugin
|
* MCP Server Project Plugin
|
||||||
@@ -55,10 +56,12 @@ class McpServerPlugin extends AbstractProjectPlugin
|
|||||||
$warnings = [];
|
$warnings = [];
|
||||||
|
|
||||||
// Check for required source files
|
// 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) {
|
foreach ($requiredSrc as $file) {
|
||||||
if (!file_exists("{$projectPath}/{$file}")) {
|
if (SourceResolver::findUnderSource($projectPath, $file) === null) {
|
||||||
$errors[] = "Missing required source file: {$file}";
|
$errors[] = "Missing required source file: {$srcName}/{$file}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,37 +85,33 @@ class McpServerPlugin extends AbstractProjectPlugin
|
|||||||
$errors[] = 'Missing tsconfig.json';
|
$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
|
// Check for config example
|
||||||
if (!file_exists("{$projectPath}/config.example.json")) {
|
if (!file_exists("{$projectPath}/config.example.json")) {
|
||||||
$warnings[] = 'Missing config.example.json — example configuration recommended';
|
$warnings[] = 'Missing config.example.json — example configuration recommended';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for shebang in index.ts
|
// Check for shebang in index.ts
|
||||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
$indexTs = SourceResolver::findUnderSource($projectPath, 'index.ts');
|
||||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
if ($indexTs !== null) {
|
||||||
|
$content = @file_get_contents($indexTs);
|
||||||
if ($content && strpos($content, '#!/usr/bin/env node') === false) {
|
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
|
// Check for McpServer usage
|
||||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
if ($indexTs !== null) {
|
||||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
$content = $content ?? @file_get_contents($indexTs);
|
||||||
if ($content && strpos($content, 'McpServer') === false) {
|
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
|
// Check for StdioServerTransport
|
||||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
if ($indexTs !== null) {
|
||||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
$content = $content ?? @file_get_contents($indexTs);
|
||||||
if ($content && strpos($content, 'StdioServerTransport') === false) {
|
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;
|
$score = 100;
|
||||||
|
|
||||||
// Check for required source files
|
// 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) {
|
foreach ($requiredSrc as $file) {
|
||||||
if (!file_exists("{$projectPath}/{$file}")) {
|
if (SourceResolver::findUnderSource($projectPath, $file) === null) {
|
||||||
$issues[] = [
|
$issues[] = [
|
||||||
'severity' => 'critical',
|
'severity' => 'critical',
|
||||||
'message' => "Missing required file: {$file}",
|
'message' => "Missing required file: {$srcName}/{$file}",
|
||||||
];
|
];
|
||||||
$score -= 20;
|
$score -= 20;
|
||||||
}
|
}
|
||||||
@@ -214,14 +214,15 @@ class McpServerPlugin extends AbstractProjectPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for at least one registered tool
|
// Check for at least one registered tool
|
||||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
$indexTs = SourceResolver::findUnderSource($projectPath, 'index.ts');
|
||||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
if ($indexTs !== null) {
|
||||||
|
$content = @file_get_contents($indexTs);
|
||||||
if ($content) {
|
if ($content) {
|
||||||
$toolCount = substr_count($content, 'server.tool(');
|
$toolCount = substr_count($content, 'server.tool(');
|
||||||
if ($toolCount === 0) {
|
if ($toolCount === 0) {
|
||||||
$issues[] = [
|
$issues[] = [
|
||||||
'severity' => 'critical',
|
'severity' => 'critical',
|
||||||
'message' => 'No MCP tools registered in src/index.ts',
|
'message' => "No MCP tools registered in {$srcName}/index.ts",
|
||||||
];
|
];
|
||||||
$score -= 25;
|
$score -= 25;
|
||||||
} elseif ($toolCount < 5) {
|
} elseif ($toolCount < 5) {
|
||||||
|
|||||||
@@ -173,37 +173,15 @@ class RepositorySynchronizer
|
|||||||
$platform = $this->detectPlatform($repoInfo);
|
$platform = $this->detectPlatform($repoInfo);
|
||||||
$this->logger->logInfo("Detected platform for {$repo}: {$platform}");
|
$this->logger->logInfo("Detected platform for {$repo}: {$platform}");
|
||||||
|
|
||||||
// Load file list from the Terraform definition for this platform
|
// Load shared workflows and config files for this platform from templates
|
||||||
$filesToSync = $this->definitionParser->parseForPlatform($platform, $repoRoot);
|
$filesToSync = $this->getSharedWorkflows($platform, $repoRoot);
|
||||||
|
$sharedTotal = count($filesToSync);
|
||||||
// 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);
|
|
||||||
$this->logger->logInfo(
|
$this->logger->logInfo(
|
||||||
"Loaded " . count($filesToSync) . " sync entries for {$platform}"
|
"Loaded {$sharedTotal} sync entries for {$platform}"
|
||||||
. " (def={$defCount}, shared={$sharedAdded}/{$sharedTotal} added, "
|
|
||||||
. ($sharedTotal - $sharedAdded) . " deduped)"
|
|
||||||
);
|
);
|
||||||
// Log shared workflow destinations for debugging
|
foreach ($filesToSync as $sf) {
|
||||||
foreach ($sharedFiles as $sf) {
|
|
||||||
$dest = $sf['destination'] ?? '?';
|
$dest = $sf['destination'] ?? '?';
|
||||||
$added = !isset($seen[$dest]) ? 'ADDED' : 'DEDUPED';
|
$this->logger->logInfo(" sync: {$dest}");
|
||||||
$this->logger->logInfo(" shared: {$dest} [{$added}]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($filesToSync)) {
|
if (empty($filesToSync)) {
|
||||||
@@ -1380,7 +1358,7 @@ class RepositorySynchronizer
|
|||||||
|
|
||||||
$descriptors = array_values(array_filter(
|
$descriptors = array_values(array_filter(
|
||||||
$paths,
|
$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)) {
|
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,56 +0,0 @@
|
|||||||
# Update Server — Dolibarr Modules
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
MokoGitea provides a built-in Update Server that can serve Dolibarr-compatible JSON update feeds from repository releases. **No static feed file is needed in the repository.**
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. **Enable Update Server** in the repository's Settings > Advanced Settings
|
|
||||||
2. **Configure metadata** in Settings > Update Server (set platform to `dolibarr`)
|
|
||||||
3. **Create releases** with tagged module archives
|
|
||||||
4. MokoGitea serves the update feed at `/{owner}/{repo}/updates/dolibarr.json`
|
|
||||||
|
|
||||||
## Feed URL
|
|
||||||
|
|
||||||
```
|
|
||||||
https://git.mokoconsulting.tech/{owner}/{repo}/updates/dolibarr.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release Naming Convention
|
|
||||||
|
|
||||||
Release assets should follow:
|
|
||||||
|
|
||||||
```
|
|
||||||
{module_name}-{version}.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- `mokocrm-18.0.1.zip`
|
|
||||||
- `mokodolisign-3.2.0.zip`
|
|
||||||
|
|
||||||
## Update Server Settings
|
|
||||||
|
|
||||||
Configure these in Settings > Update Server:
|
|
||||||
|
|
||||||
| Field | Description | Example |
|
|
||||||
|-------|-------------|---------|
|
|
||||||
| Platform | Set to `dolibarr` | `dolibarr` |
|
|
||||||
| Extension Name | Dolibarr module directory name | `mokocrm` |
|
|
||||||
| Display Name | Human-readable name | `Module - MokoCRM` |
|
|
||||||
| Extension Type | Usually `module` | `module` |
|
|
||||||
| Maintainer | Organization name | `Moko Consulting` |
|
|
||||||
| Support URL | Product support page | `https://mokoconsulting.tech/support/mokocrm` |
|
|
||||||
|
|
||||||
## Download Gating
|
|
||||||
|
|
||||||
Same three modes as Joomla: `none`, `prerelease`, `all`.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Dolibarr Update Server support is currently **disabled for all modules except MokoCRM**. Metadata is pre-configured and ready to enable when needed.
|
|
||||||
|
|
||||||
## What NOT to Do
|
|
||||||
|
|
||||||
- **Do NOT commit static feed files to the repository**
|
|
||||||
- **Do NOT use legacy update check mechanisms** — use the built-in feed
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# Update Server — Joomla Extensions
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
MokoGitea provides a built-in Update Server that dynamically generates Joomla-compatible update XML feeds from repository releases. **No static `updates.xml` file is needed in the repository.**
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. **Enable Update Server** in the repository's Settings > Advanced Settings
|
|
||||||
2. **Configure metadata** in Settings > Update Server (extension name, type, target version, etc.)
|
|
||||||
3. **Create releases** with tagged assets (e.g. `pkg_mokowaas-02.19.00.zip`)
|
|
||||||
4. MokoGitea automatically serves the update feed at `/{owner}/{repo}/updates.xml`
|
|
||||||
|
|
||||||
## Feed URL
|
|
||||||
|
|
||||||
```
|
|
||||||
https://git.mokoconsulting.tech/{owner}/{repo}/updates.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
This URL is what goes into your Joomla extension's `update_server` element in the manifest XML.
|
|
||||||
|
|
||||||
## Manifest Configuration
|
|
||||||
|
|
||||||
In your extension's manifest XML (`*.xml`), add:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<updateservers>
|
|
||||||
<server type="extension" name="{Extension Name}">
|
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/{RepoName}/updates.xml
|
|
||||||
</server>
|
|
||||||
</updateservers>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release Naming Convention
|
|
||||||
|
|
||||||
Release assets must follow this naming pattern for the feed generator to detect them:
|
|
||||||
|
|
||||||
```
|
|
||||||
{extension_name}-{version}.zip
|
|
||||||
{extension_name}-{version}.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- `pkg_mokowaas-02.19.00.zip`
|
|
||||||
- `tpl_mokoonyx-02.19.00.zip`
|
|
||||||
- `mod_mokojoomhero-01.05.00.zip`
|
|
||||||
|
|
||||||
## Update Server Settings
|
|
||||||
|
|
||||||
Configure these in Settings > Update Server:
|
|
||||||
|
|
||||||
| Field | Description | Example |
|
|
||||||
|-------|-------------|---------|
|
|
||||||
| Extension Name | Joomla element name | `pkg_mokowaas` |
|
|
||||||
| Display Name | Human-readable name | `Package - MokoWaaS` |
|
|
||||||
| Extension Type | package, plugin, template, module, component | `package` |
|
|
||||||
| Target Version | Regex for compatible Joomla versions | `(5|6)\..*` |
|
|
||||||
| PHP Minimum | Minimum PHP version | `8.1` |
|
|
||||||
| Maintainer | Organization name | `Moko Consulting` |
|
|
||||||
| Maintainer URL | Organization website | `https://mokoconsulting.tech` |
|
|
||||||
| Support URL | Product support page | `https://mokoconsulting.tech/products/{alias}` |
|
|
||||||
| Info URL | Product information page | `https://mokoconsulting.tech/products/{alias}` |
|
|
||||||
|
|
||||||
## Download Gating
|
|
||||||
|
|
||||||
Three modes control who can download release assets:
|
|
||||||
|
|
||||||
| Mode | Behavior |
|
|
||||||
|------|----------|
|
|
||||||
| `none` | All downloads are public |
|
|
||||||
| `prerelease` | Pre-release downloads require a license key; stable releases are public |
|
|
||||||
| `all` | All downloads require a license key |
|
|
||||||
|
|
||||||
The update feed XML is **always public** — only the actual file downloads are gated.
|
|
||||||
|
|
||||||
## What NOT to Do
|
|
||||||
|
|
||||||
- **Do NOT commit `updates.xml` to the repository** — it is served dynamically
|
|
||||||
- **Do NOT use static update server workflows** — the old CI-generated approach is deprecated
|
|
||||||
- **Do NOT hardcode version numbers in feed URLs** — the feed auto-detects from releases
|
|
||||||
|
|
||||||
## Changelog Feed
|
|
||||||
|
|
||||||
A changelog XML is also served automatically at:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://git.mokoconsulting.tech/{owner}/{repo}/changelog.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
This is generated from release notes (markdown body of each release).
|
|
||||||
@@ -1,366 +1,34 @@
|
|||||||
<!--
|
# {{REPO_NAME}}
|
||||||
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_DESCRIPTION}}
|
{{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) |
|
||||||
|
|
||||||
---
|
## Commands
|
||||||
|
|
||||||
# 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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
composer install # first time
|
make build # Build the project
|
||||||
composer update mokoconsulting/mokostandards # upgrade
|
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
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
#!/usr/bin/env php
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
<?php
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
*
|
- **Standards**: [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
* 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 |
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ use MokoEnterprise\{
|
|||||||
PluginFactory,
|
PluginFactory,
|
||||||
PluginRegistry,
|
PluginRegistry,
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
MetricsCollector
|
MetricsCollector,
|
||||||
|
SourceResolver
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -228,8 +229,9 @@ class AutoDetectPlatform extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy: site structure inside src/
|
// Legacy: site structure inside source/ or src/
|
||||||
$siteDirs = ['src/administrator', 'src/components', 'src/plugins', 'src/templates', 'src/media'];
|
$srcName = SourceResolver::resolve($repoPath);
|
||||||
|
$siteDirs = ["{$srcName}/administrator", "{$srcName}/components", "{$srcName}/plugins", "{$srcName}/templates", "{$srcName}/media"];
|
||||||
$siteDirCount = 0;
|
$siteDirCount = 0;
|
||||||
foreach ($siteDirs as $dir) {
|
foreach ($siteDirs as $dir) {
|
||||||
if (is_dir($repoPath . '/' . $dir)) {
|
if (is_dir($repoPath . '/' . $dir)) {
|
||||||
@@ -238,7 +240,7 @@ class AutoDetectPlatform extends CliFramework
|
|||||||
}
|
}
|
||||||
if ($siteDirCount >= 3) {
|
if ($siteDirCount >= 3) {
|
||||||
$score += 20;
|
$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
|
// 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
|
// Check for MCP server entry point with McpServer usage
|
||||||
if (file_exists("{$repoPath}/src/index.ts")) {
|
$mcpEntry = SourceResolver::findUnderSource($repoPath, 'index.ts');
|
||||||
$content = @file_get_contents("{$repoPath}/src/index.ts");
|
if ($mcpEntry !== null) {
|
||||||
|
$content = @file_get_contents($mcpEntry);
|
||||||
|
$mcpSrcName = SourceResolver::resolve($repoPath);
|
||||||
if ($content) {
|
if ($content) {
|
||||||
if (strpos($content, 'McpServer') !== false) {
|
if (strpos($content, 'McpServer') !== false) {
|
||||||
$score += 0.3;
|
$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) {
|
if (strpos($content, 'server.tool(') !== false) {
|
||||||
$score += 0.1;
|
$score += 0.1;
|
||||||
$toolCount = substr_count($content, 'server.tool(');
|
$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) {
|
if (strpos($content, 'StdioServerTransport') !== false) {
|
||||||
$score += 0.1;
|
$score += 0.1;
|
||||||
@@ -730,16 +734,17 @@ class AutoDetectPlatform extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for the standard 4-file MCP structure
|
// 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;
|
$foundCount = 0;
|
||||||
foreach ($mcpFiles as $file) {
|
foreach ($mcpRequired as $file) {
|
||||||
if (file_exists("{$repoPath}/{$file}")) {
|
if (SourceResolver::findUnderSource($repoPath, $file) !== null) {
|
||||||
$foundCount++;
|
$foundCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($foundCount === 4) {
|
if ($foundCount === 4) {
|
||||||
$score += 0.1;
|
$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
|
// Check for setup wizard
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use MokoEnterprise\CliFramework;
|
|||||||
class CheckChangelog extends CliFramework
|
class CheckChangelog extends CliFramework
|
||||||
{
|
{
|
||||||
/** Directories searched for CHANGELOG.md, relative to --path (case-insensitive match). */
|
/** 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.
|
* Configure available arguments.
|
||||||
@@ -57,7 +57,7 @@ class CheckChangelog extends CliFramework
|
|||||||
$found = $this->findChangelog($path);
|
$found = $this->findChangelog($path);
|
||||||
|
|
||||||
if ($found === null) {
|
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());
|
$this->printSummary(0, 1, $this->elapsed());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates client theme packages that deliver CSS, JS, and images
|
* Validates client theme packages that deliver CSS, JS, and images
|
||||||
@@ -44,17 +44,17 @@ class CheckClientTheme extends CliFramework
|
|||||||
/** Recommended XML elements. */
|
/** Recommended XML elements. */
|
||||||
private const RECOMMENDED_ELEMENTS = ['updateservers', 'scriptfile', 'description', 'fileset'];
|
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 = [
|
private const REQUIRED_THEME_FILES = [
|
||||||
'src/media/templates/site/mokoonyx/css/theme/light.custom.css',
|
'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/dark.custom.css',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Optional but expected files. */
|
/** Optional but expected files (paths prefixed with ~ are relative to source dir). */
|
||||||
private const EXPECTED_FILES = [
|
private const EXPECTED_FILES = [
|
||||||
'src/media/templates/site/mokoonyx/css/user.css',
|
'~media/templates/site/mokoonyx/css/user.css',
|
||||||
'src/media/templates/site/mokoonyx/js/user.js',
|
'~media/templates/site/mokoonyx/js/user.js',
|
||||||
'src/script.php',
|
'~script.php',
|
||||||
'updates.xml',
|
'updates.xml',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -81,10 +81,12 @@ class CheckClientTheme extends CliFramework
|
|||||||
|
|
||||||
// ── Manifest ──────────────────────────────────────────
|
// ── Manifest ──────────────────────────────────────────
|
||||||
$this->section('Manifest validation');
|
$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)) {
|
if (!is_file($manifest)) {
|
||||||
$this->status(false, 'Missing src/templateDetails.xml');
|
$this->status(false, "Missing {$srcName}/templateDetails.xml");
|
||||||
$this->printSummary(0, 1, $this->elapsed());
|
$this->printSummary(0, 1, $this->elapsed());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -144,28 +146,36 @@ class CheckClientTheme extends CliFramework
|
|||||||
// ── Required files ────────────────────────────────────
|
// ── Required files ────────────────────────────────────
|
||||||
$this->section('Required files');
|
$this->section('Required files');
|
||||||
foreach (self::REQUIRED_THEME_FILES as $file) {
|
foreach (self::REQUIRED_THEME_FILES as $file) {
|
||||||
$full = $path . '/' . $file;
|
$full = "{$path}/{$srcName}/{$file}";
|
||||||
if (is_file($full)) {
|
if (is_file($full)) {
|
||||||
$this->status(true, basename($file));
|
$this->status(true, basename($file));
|
||||||
} else {
|
} else {
|
||||||
$this->status(false, "Missing: {$file}");
|
$this->status(false, "Missing: {$srcName}/{$file}");
|
||||||
$errors++;
|
$errors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (self::EXPECTED_FILES as $file) {
|
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)) {
|
if (is_file($full)) {
|
||||||
$this->status(true, basename($file));
|
$this->status(true, basename($file));
|
||||||
} else {
|
} else {
|
||||||
$this->warning("Missing: {$file}");
|
$this->warning("Missing: {$display}");
|
||||||
$warns++;
|
$warns++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PHP syntax ────────────────────────────────────────
|
// ── PHP syntax ────────────────────────────────────────
|
||||||
$this->section('PHP syntax');
|
$this->section('PHP syntax');
|
||||||
$phpFiles = glob($path . '/src/*.php') ?: [];
|
$phpFiles = glob("{$path}/{$srcName}/*.php") ?: [];
|
||||||
foreach ($phpFiles as $phpFile) {
|
foreach ($phpFiles as $phpFile) {
|
||||||
$output = [];
|
$output = [];
|
||||||
$ret = 0;
|
$ret = 0;
|
||||||
@@ -179,20 +189,20 @@ class CheckClientTheme extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (empty($phpFiles)) {
|
if (empty($phpFiles)) {
|
||||||
$this->warning('No PHP files in src/');
|
$this->warning("No PHP files in {$srcName}/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CSS validation ────────────────────────────────────
|
// ── CSS validation ────────────────────────────────────
|
||||||
$this->section('CSS validation');
|
$this->section('CSS validation');
|
||||||
$cssFiles = array_merge(
|
$cssFiles = array_merge(
|
||||||
glob($path . '/src/media/templates/site/mokoonyx/css/theme/*.css') ?: [],
|
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/theme/*.css") ?: [],
|
||||||
glob($path . '/src/media/templates/site/mokoonyx/css/*.css') ?: [],
|
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/*.css") ?: [],
|
||||||
);
|
);
|
||||||
foreach ($cssFiles as $cssFile) {
|
foreach ($cssFiles as $cssFile) {
|
||||||
$css = (string) file_get_contents($cssFile);
|
$css = (string) file_get_contents($cssFile);
|
||||||
$open = substr_count($css, '{');
|
$open = substr_count($css, '{');
|
||||||
$close = substr_count($css, '}');
|
$close = substr_count($css, '}');
|
||||||
$name = str_replace($path . '/src/', '', $cssFile);
|
$name = str_replace("{$path}/{$srcName}/", '', $cssFile);
|
||||||
|
|
||||||
if ($open !== $close) {
|
if ($open !== $close) {
|
||||||
$this->status(false, "Unbalanced braces in {$name} (open: {$open}, close: {$close})");
|
$this->status(false, "Unbalanced braces in {$name} (open: {$open}, close: {$close})");
|
||||||
@@ -241,7 +251,7 @@ class CheckClientTheme extends CliFramework
|
|||||||
// ── Image sizes ───────────────────────────────────────
|
// ── Image sizes ───────────────────────────────────────
|
||||||
$this->section('Image optimization');
|
$this->section('Image optimization');
|
||||||
$largeImages = 0;
|
$largeImages = 0;
|
||||||
$imageDir = $path . '/src/images';
|
$imageDir = "{$path}/{$srcName}/images";
|
||||||
if (is_dir($imageDir)) {
|
if (is_dir($imageDir)) {
|
||||||
$iter = new \RecursiveIteratorIterator(
|
$iter = new \RecursiveIteratorIterator(
|
||||||
new \RecursiveDirectoryIterator($imageDir, \FilesystemIterator::SKIP_DOTS)
|
new \RecursiveDirectoryIterator($imageDir, \FilesystemIterator::SKIP_DOTS)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the required directory structure of a Dolibarr module repository.
|
* Validates the required directory structure of a Dolibarr module repository.
|
||||||
@@ -47,33 +47,36 @@ class CheckDolibarrModule extends CliFramework
|
|||||||
$failed = 0;
|
$failed = 0;
|
||||||
|
|
||||||
$this->section('Checking directory structure');
|
$this->section('Checking directory structure');
|
||||||
|
$srcName = SourceResolver::resolve($path);
|
||||||
|
SourceResolver::warnIfLegacy($path);
|
||||||
|
|
||||||
if (!is_dir($path . '/src')) {
|
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||||
$this->status(false, 'src/ directory exists');
|
if ($srcDir === null) {
|
||||||
|
$this->status(false, 'source/ or src/ directory exists');
|
||||||
$failed++;
|
$failed++;
|
||||||
} else {
|
} else {
|
||||||
$this->status(true, 'src/ directory exists');
|
$this->status(true, "{$srcName}/ directory exists");
|
||||||
$passed++;
|
$passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_dir($path . '/src/core/modules')) {
|
if (!is_dir($path . "/{$srcName}/core/modules")) {
|
||||||
$this->status(false, 'src/core/modules/ directory exists');
|
$this->status(false, "{$srcName}/core/modules/ directory exists");
|
||||||
$failed++;
|
$failed++;
|
||||||
} else {
|
} else {
|
||||||
$this->status(true, 'src/core/modules/ directory exists');
|
$this->status(true, "{$srcName}/core/modules/ directory exists");
|
||||||
$passed++;
|
$passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_dir($path . '/src/langs')) {
|
if (!is_dir($path . "/{$srcName}/langs")) {
|
||||||
$this->warning('Missing suggested directory: src/langs/');
|
$this->warning("Missing suggested directory: {$srcName}/langs/");
|
||||||
} else {
|
} else {
|
||||||
$this->status(true, 'src/langs/ directory exists');
|
$this->status(true, "{$srcName}/langs/ directory exists");
|
||||||
$passed++;
|
$passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->section('Checking module descriptor');
|
$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)) {
|
if (empty($descriptors)) {
|
||||||
$this->status(false, 'Module descriptor found (mod*.class.php)');
|
$this->status(false, 'Module descriptor found (mod*.class.php)');
|
||||||
$failed++;
|
$failed++;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class CheckStructure extends CliFramework
|
|||||||
private const REQUIRED_FILES = ['README.md', 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md'];
|
private const REQUIRED_FILES = ['README.md', 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md'];
|
||||||
|
|
||||||
/** Directories searched for CHANGELOG.md (case-insensitive), relative to repo root. */
|
/** 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.
|
* Configure available arguments.
|
||||||
|
|||||||
Reference in New Issue
Block a user