diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 90cdf7c..e849522 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 09.25.00 +# VERSION: 09.25.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 86908c2..9615a4e 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,243 +1,243 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - pull_request_target: - types: [synchronize, opened, reopened] - branches: - - main - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - 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 }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || - (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - # Use pre-installed /opt/moko-platform if available (updated by cron every 6h) - if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then - echo Using pre-installed /opt/moko-platform - echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV - else - echo Falling back to fresh clone - if ! command -v composer > /dev/null 2>&1; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 - fi - rm -rf /tmp/moko-platform-api - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV - fi - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - # Auto-detect stability: RC for PRs targeting main, else use input or default to development - if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then - STABILITY="release-candidate" - else - STABILITY="${{ inputs.stability || 'development' }}" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Bump version via CLI: patch for dev/alpha/beta, minor for RC - case "$STABILITY" in - release-candidate) BUMP="minor" ;; - *) BUMP="patch" ;; - esac - - php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true - - # Set stability suffix and verify consistency - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml - php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true - - # Append suffix for output - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - 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" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Update release notes from CHANGELOG.md - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - else - NOTES="Release ${VERSION}" - fi - - # Update release body via API - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ]; then - python3 -c " - import json, urllib.request - body = open('/dev/stdin').read() - payload = json.dumps({'body': body}).encode() - req = urllib.request.Request( - '${API_BASE}/releases/${RELEASE_ID}', - data=payload, method='PATCH', - headers={ - 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - 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 - - # updates.xml is generated dynamically by MokoGitea license server - # No need to build, commit, or sync updates.xml from workflows - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + pull_request_target: + types: [synchronize, opened, reopened] + branches: + - main + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + 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 }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + # Use pre-installed /opt/moko-platform if available (updated by cron every 6h) + if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then + echo Using pre-installed /opt/moko-platform + echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/moko-platform-api + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV + fi + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + # Auto-detect stability: RC for PRs targeting main, else use input or default to development + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then + STABILITY="release-candidate" + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Bump version via CLI: patch for dev/alpha/beta, minor for RC + case "$STABILITY" in + release-candidate) BUMP="minor" ;; + *) BUMP="patch" ;; + esac + + php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true + + # Set stability suffix and verify consistency + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml + php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true + + # Append suffix for output + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Update release notes from CHANGELOG.md + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + else + NOTES="Release ${VERSION}" + fi + + # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ]; then + python3 -c " + import json, urllib.request + body = open('/dev/stdin').read() + payload = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=payload, method='PATCH', + headers={ + 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + 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 + + # updates.xml is generated dynamically by MokoGitea license server + # No need to build, commit, or sync updates.xml from workflows + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 989673c..3e52eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ BRIEF: Release changelog # Changelog ## [Unreleased] +### Added +- `workflow_sync.php` — cascading workflow sync from Generic → platform templates → live repos based on manifest.platform +- `platform_detect.php` — auto-detect repo platform type (joomla/dolibarr/go/mcp/platform/generic) from file structure, optionally update manifest +- Version prefix support in `version_read.php` and `version_bump.php` — repos with `` in manifest (e.g. MokoGitea: `1.26.1+moko.`) get prefix-aware version scanning and bumping +- Platform types: joomla, dolibarr, go, mcp, platform, generic +- Template-Go and Template-MCP repos created + +### Changed +- `auto-release.yml` — patch branches (fix/*, patch/*, hotfix/*, bugfix/*) use `--bump none` (pre-release already bumped); feature/dev branches bump minor +- `pre-release.yml` — triggers on push to dev, fix/**, patch/**, hotfix/**, bugfix/**, alpha, beta, rc branches +- Version format standardized: `[prefix]XX.YY.ZZ` in source files, suffix (`-dev`, `-rc`) added by release system only ## [09.25.00] --- 2026-06-04 diff --git a/README.md b/README.md index 0f13449..c6bf850 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root INGROUP: MokoPlatform REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform PATH: /README.md -VERSION: 09.25.00 +VERSION: 09.25.02 BRIEF: Project overview and documentation --> diff --git a/cli/branch_rename.php b/cli/branch_rename.php index 5564d66..61b7967 100644 --- a/cli/branch_rename.php +++ b/cli/branch_rename.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/branch_rename.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old) */ diff --git a/cli/bulk_workflow_push.php b/cli/bulk_workflow_push.php index 62dd98e..cc56f77 100644 --- a/cli/bulk_workflow_push.php +++ b/cli/bulk_workflow_push.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/bulk_workflow_push.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Push a workflow file to all governed repos via the Gitea Contents API */ diff --git a/cli/bulk_workflow_trigger.php b/cli/bulk_workflow_trigger.php index 27a2460..3f4df33 100644 --- a/cli/bulk_workflow_trigger.php +++ b/cli/bulk_workflow_trigger.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/bulk_workflow_trigger.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Trigger a workflow across multiple repos at once */ diff --git a/cli/client_dashboard.php b/cli/client_dashboard.php index 423854b..3fb51e5 100644 --- a/cli/client_dashboard.php +++ b/cli/client_dashboard.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_dashboard.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Generate unified client dashboard HTML */ diff --git a/cli/client_inventory.php b/cli/client_inventory.php index e9ad6f0..b40f30c 100644 --- a/cli/client_inventory.php +++ b/cli/client_inventory.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_inventory.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Discover and list all client-waas repos with their server configuration status */ diff --git a/cli/client_provision.php b/cli/client_provision.php index 36418ae..fdfa4d1 100644 --- a/cli/client_provision.php +++ b/cli/client_provision.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_provision.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Provision a new client environment end-to-end */ diff --git a/cli/grafana_dashboard.php b/cli/grafana_dashboard.php index 19fe307..596f776 100644 --- a/cli/grafana_dashboard.php +++ b/cli/grafana_dashboard.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/grafana_dashboard.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Manage Grafana dashboards via API */ diff --git a/cli/joomla_build.php b/cli/joomla_build.php index 34d8dbf..66f4a5b 100644 --- a/cli/joomla_build.php +++ b/cli/joomla_build.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/joomla_build.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Build a Joomla extension ZIP from manifest — all types supported * NOTE: Called by pre-release and auto-release workflows. */ diff --git a/cli/manifest_licensing.php b/cli/manifest_licensing.php index 3ff7ecc..5c8096b 100644 --- a/cli/manifest_licensing.php +++ b/cli/manifest_licensing.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/manifest_licensing.php - * VERSION: 01.00.00 + * VERSION: 09.25.02 * BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests */ diff --git a/cli/manifest_read.php b/cli/manifest_read.php index 2fd71ab..3572920 100644 --- a/cli/manifest_read.php +++ b/cli/manifest_read.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/manifest_read.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption */ diff --git a/cli/platform_detect.php b/cli/platform_detect.php index 15f8ffe..b07c6cb 100644 --- a/cli/platform_detect.php +++ b/cli/platform_detect.php @@ -10,7 +10,8 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/platform_detect.php - * BRIEF: Detect platform from manifest.xml file — outputs platform string + * VERSION: 09.25.02 + * BRIEF: Auto-detect repository platform type and optionally update manifest */ declare(strict_types=1); @@ -23,8 +24,14 @@ class PlatformDetectCli extends CliFramework { protected function configure(): void { - $this->setDescription('Detect platform from manifest.xml file'); - $this->addArgument('--path', 'Repository root path', '.'); + $this->setDescription('Auto-detect repository platform type and optionally update manifest'); + $this->addArgument('--path', 'Local repo path to scan (default: .)', '.'); + $this->addArgument('--token', 'Gitea API token for updating manifest', ''); + $this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech'); + $this->addArgument('--owner', 'Repo owner for API update', ''); + $this->addArgument('--repo', 'Repo name for API update', ''); + $this->addArgument('--update', 'Update manifest.platform via API (flag)', 'false'); + $this->addArgument('--github-output', 'Append platform=xxx to $GITHUB_OUTPUT (flag)', 'false'); } protected function run(): int @@ -32,25 +39,161 @@ class PlatformDetectCli extends CliFramework $path = $this->getArgument('--path'); $root = realpath($path) ?: $path; - // Check .mokogitea/manifest.xml first, fallback to root - $file = "{$root}/.mokogitea/manifest.xml"; - if (!file_exists($file)) { - $file = "{$root}/.mokostandards"; - } - if (!file_exists($file)) { - echo "unknown\n"; - return 0; + $token = $this->getArgument('--token'); + $giteaUrl = rtrim($this->getArgument('--gitea-url'), '/'); + $owner = $this->getArgument('--owner'); + $repo = $this->getArgument('--repo'); + $doUpdate = $this->isFlagSet('--update'); + $githubOutput = $this->isFlagSet('--github-output'); + + $platform = $this->detectPlatform($root); + + $this->log('INFO', "Detected platform: {$platform}"); + echo $platform . "\n"; + + // Append to $GITHUB_OUTPUT if requested + if ($githubOutput) { + $outputFile = getenv('GITHUB_OUTPUT'); + + if ($outputFile !== false && $outputFile !== '') { + file_put_contents($outputFile, "platform={$platform}\n", FILE_APPEND); + $this->log('INFO', "Appended platform={$platform} to \$GITHUB_OUTPUT"); + } else { + $this->log('WARN', '$GITHUB_OUTPUT is not set; skipping output append.'); + } } - $content = file_get_contents($file); - if (preg_match('/^platform:\s*(.+)/m', $content, $m)) { - echo trim($m[1], " \t\n\r\"'") . "\n"; - } else { - echo "unknown\n"; + // Update manifest via API if requested + if ($doUpdate) { + if ($token === '' || $owner === '' || $repo === '') { + $this->log('ERROR', '--update requires --token, --owner, and --repo.'); + return 1; + } + + if ($this->dryRun) { + $this->log('INFO', "[DRY RUN] Would update manifest.platform to \"{$platform}\" " + . "for {$owner}/{$repo}."); + return 0; + } + + $this->log('INFO', "Updating manifest.platform for {$owner}/{$repo} to \"{$platform}\"..."); + + $response = $this->apiRequest( + $giteaUrl, + $token, + 'PATCH', + "/api/v1/repos/{$owner}/{$repo}/manifest", + json_encode(['platform' => $platform]) + ); + + if ($response['code'] >= 200 && $response['code'] < 300) { + $this->log('INFO', "Manifest updated successfully (HTTP {$response['code']})."); + } else { + $this->log('ERROR', "Failed to update manifest (HTTP {$response['code']}): " + . $response['body']); + return 1; + } } return 0; } + + private function detectPlatform(string $root): string + { + // 1. Joomla — has pkg_*.xml or Joomla-style extension manifest + $joomlaIndicators = array_merge( + glob("{$root}/source/pkg_*.xml") ?: [], + glob("{$root}/pkg_*.xml") ?: [], + glob("{$root}/source/packages/*/services/provider.php") ?: [], + glob("{$root}/**/templateDetails.xml") ?: [], + ); + if (!empty($joomlaIndicators)) { + return 'joomla'; + } + + // 2. Dolibarr — has mod*.class.php or dolibarr module descriptor + $doliIndicators = array_merge( + glob("{$root}/core/modules/mod*.class.php") ?: [], + glob("{$root}/class/*.class.php") ?: [], + ); + if (!empty($doliIndicators) && file_exists("{$root}/langs")) { + return 'dolibarr'; + } + + // 3. Go — has go.mod + if (file_exists("{$root}/go.mod")) { + return 'go'; + } + + // 4. MCP — has package.json with mcp-related content or dist/index.js pattern + if (file_exists("{$root}/package.json")) { + $pkg = json_decode(file_get_contents("{$root}/package.json"), true); + $name = $pkg['name'] ?? ''; + if (str_contains($name, 'mcp') || isset($pkg['dependencies']['@modelcontextprotocol/sdk'])) { + return 'mcp'; + } + } + + // 5. Platform — is mokoplatform itself or org-config + $repoName = basename($root); + if (in_array($repoName, ['mokoplatform', 'mokogitea-org-config'])) { + return 'platform'; + } + + // 6. Default + return 'generic'; + } + + private function isFlagSet(string $flag): bool + { + $value = $this->getArgument($flag); + + return $value === 'true' || $value === '1' || $value === 'yes'; + } + + private function apiRequest( + string $giteaUrl, + string $token, + string $method, + string $endpoint, + ?string $body = null + ): array { + $url = $giteaUrl . $endpoint; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Accept: application/json', + "Authorization: token {$token}", + ]); + + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $responseBody = curl_exec($ch); + $httpCode = (int) curl_getinfo( + $ch, + CURLINFO_HTTP_CODE + ); + + if (curl_errno($ch)) { + $error = curl_error($ch); + curl_close($ch); + + return [ + 'code' => 0, + 'body' => "cURL error: {$error}", + ]; + } + + curl_close($ch); + + return ['code' => $httpCode, 'body' => $responseBody]; + } } $app = new PlatformDetectCli(); diff --git a/cli/release_cascade.php b/cli/release_cascade.php index 44135ff..432947e 100644 --- a/cli/release_cascade.php +++ b/cli/release_cascade.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/release_cascade.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent. */ diff --git a/cli/release_publish.php b/cli/release_publish.php index d914a6e..7b12819 100644 --- a/cli/release_publish.php +++ b/cli/release_publish.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/release_publish.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Publish a release and create copies for all lesser stability streams. */ diff --git a/cli/scaffold_client.php b/cli/scaffold_client.php index 1c6b070..0bd2998 100644 --- a/cli/scaffold_client.php +++ b/cli/scaffold_client.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/scaffold_client.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings */ diff --git a/cli/updates_xml_sync.php b/cli/updates_xml_sync.php index c70f4d3..193d9d4 100644 --- a/cli/updates_xml_sync.php +++ b/cli/updates_xml_sync.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/updates_xml_sync.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Sync updates.xml to target branches via Gitea API * NOTE: Called by pre-release and auto-release workflows after updates.xml * is modified on the current branch. Pushes the file to other branches diff --git a/cli/version_auto_bump.php b/cli/version_auto_bump.php index ca8904c..9333d53 100644 --- a/cli/version_auto_bump.php +++ b/cli/version_auto_bump.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/version_auto_bump.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash */ diff --git a/cli/version_bump.php b/cli/version_bump.php index 79d7bab..b8698f7 100644 --- a/cli/version_bump.php +++ b/cli/version_bump.php @@ -42,6 +42,7 @@ class VersionBumpCli extends CliFramework $root = realpath($path) ?: $path; $mokoVersion = null; $existingSuffix = ''; + $versionPrefix = ''; $mokoManifest = "{$root}/.mokogitea/manifest.xml"; $mokoContent = ''; if (file_exists($mokoManifest)) { @@ -50,13 +51,29 @@ class VersionBumpCli extends CliFramework $mokoVersion = $m[1]; $existingSuffix = $m[2] ?? ''; } + // Read version_prefix from manifest.xml (supports nested and flat structure) + $xml = @simplexml_load_file($mokoManifest); + if ($xml !== false) { + $prefix = (string)($xml->identity->version_prefix ?? ''); + if ($prefix === '') { + $prefix = (string)($xml->version_prefix ?? ''); + } + $versionPrefix = $prefix; + } } $readmeVersion = null; $readme = "{$root}/README.md"; $readmeContent = ''; if (file_exists($readme)) { $readmeContent = file_get_contents($readme); - if (preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) { + if (!empty($versionPrefix)) { + // Prefix-aware README scan + $prefixPattern = preg_quote($versionPrefix, '/'); + if (preg_match('/' . $prefixPattern . '(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) { + $readmeVersion = $m[1]; + } + } + if ($readmeVersion === null && preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) { $readmeVersion = $m[1]; } } @@ -73,7 +90,19 @@ class VersionBumpCli extends CliFramework $xmlContent = file_get_contents($xmlFile); if (strpos($xmlContent, '') === false) { continue; - } if (preg_match('#(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?#', $xmlContent, $xm)) { + } + if (!empty($versionPrefix)) { + // Prefix-aware: look for prefix + XX.YY.ZZ + $prefixPattern = preg_quote($versionPrefix, '#'); + if (preg_match('#' . $prefixPattern . '(\d{2}\.\d{2}\.\d{2})#', $xmlContent, $xm)) { + $candidate = $xm[1]; + if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) { + $manifestVersion = $candidate; + } + continue; + } + } + if (preg_match('#(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?#', $xmlContent, $xm)) { $candidate = $xm[1]; if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) { $manifestVersion = $candidate; @@ -136,7 +165,13 @@ class VersionBumpCli extends CliFramework } } if (file_exists($readme) && !empty($readmeContent)) { - $updated = preg_replace('/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m', '${1}' . $newBase, $readmeContent, 1); + if (!empty($versionPrefix)) { + // Prefix-aware README replacement: preserve prefix, replace only version part + $prefixPattern = preg_quote($versionPrefix, '/'); + $updated = preg_replace('/(' . $prefixPattern . ')\d{2}\.\d{2}\.\d{2}/m', '${1}' . $newBase, $readmeContent, 1); + } else { + $updated = preg_replace('/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m', '${1}' . $newBase, $readmeContent, 1); + } if ($updated !== null) { file_put_contents($readme, $updated); } @@ -149,13 +184,24 @@ class VersionBumpCli extends CliFramework if (strpos($content, '#'; - $newContent = preg_replace( - $xmlPattern, - "{$newFull}", - $content - ); + if (!empty($versionPrefix)) { + // Prefix-aware: preserve prefix, replace only the Moko version part + $prefixPattern = preg_quote($versionPrefix, '#'); + $xmlPattern = '#(' . $prefixPattern . ')\d{2}\.\d{2}\.\d{2}#'; + $newContent = preg_replace( + $xmlPattern, + '${1}' . $newBase . '', + $content + ); + } else { + $xmlPattern = '#\d{2}\.\d{2}\.\d{2}' + . '(?:(?:-(?:dev|alpha|beta|rc))+)?#'; + $newContent = preg_replace( + $xmlPattern, + "{$newFull}", + $content + ); + } if ($newContent !== null && $newContent !== $content) { file_put_contents($xmlFile, $newContent); $updatedFiles[] = substr($xmlFile, strlen($root) + 1); @@ -168,13 +214,24 @@ class VersionBumpCli extends CliFramework $packageJsonFile = "{$root}/package.json"; if (file_exists($packageJsonFile)) { $pkgContent = file_get_contents($packageJsonFile); - $pkgPattern = '/("version"\s*:\s*")\d{2}\.\d{2}\.\d{2}' - . '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m'; - $updatedPkg = preg_replace( - $pkgPattern, - '${1}' . $newFull . '${2}', - $pkgContent - ); + if (!empty($versionPrefix)) { + // Prefix-aware package.json replacement + $prefixPattern = preg_quote($versionPrefix, '/'); + $pkgPattern = '/("version"\s*:\s*")' . $prefixPattern . '\d{2}\.\d{2}\.\d{2}(")/m'; + $updatedPkg = preg_replace( + $pkgPattern, + '${1}' . $versionPrefix . $newBase . '${2}', + $pkgContent + ); + } else { + $pkgPattern = '/("version"\s*:\s*")\d{2}\.\d{2}\.\d{2}' + . '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m'; + $updatedPkg = preg_replace( + $pkgPattern, + '${1}' . $newFull . '${2}', + $pkgContent + ); + } if ($updatedPkg !== $pkgContent) { file_put_contents($packageJsonFile, $updatedPkg); fwrite(STDERR, "Updated package.json\n"); @@ -183,13 +240,24 @@ class VersionBumpCli extends CliFramework $pyprojectFile = "{$root}/pyproject.toml"; if (file_exists($pyprojectFile)) { $pyContent = file_get_contents($pyprojectFile); - $pyPattern = '/^(version\s*=\s*")\d{2}\.\d{2}\.\d{2}' - . '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m'; - $updatedPy = preg_replace( - $pyPattern, - '${1}' . $newFull . '${2}', - $pyContent - ); + if (!empty($versionPrefix)) { + // Prefix-aware pyproject.toml replacement + $prefixPattern = preg_quote($versionPrefix, '/'); + $pyPattern = '/^(version\s*=\s*")' . $prefixPattern . '\d{2}\.\d{2}\.\d{2}(")/m'; + $updatedPy = preg_replace( + $pyPattern, + '${1}' . $versionPrefix . $newBase . '${2}', + $pyContent + ); + } else { + $pyPattern = '/^(version\s*=\s*")\d{2}\.\d{2}\.\d{2}' + . '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m'; + $updatedPy = preg_replace( + $pyPattern, + '${1}' . $newFull . '${2}', + $pyContent + ); + } if ($updatedPy !== $pyContent) { file_put_contents($pyprojectFile, $updatedPy); fwrite(STDERR, "Updated pyproject.toml\n"); @@ -206,7 +274,13 @@ class VersionBumpCli extends CliFramework } $scanExtensions = ['php', 'yml', 'yaml', 'md', 'txt', 'xml', 'sh', 'toml', 'ini', 'css', 'js']; $excludeDirs = ['.git', 'vendor', 'node_modules', 'build', 'dist', '.claude']; - $versionPattern = '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m'; + // Build the generic VERSION: pattern — prefix-aware if configured + if (!empty($versionPrefix)) { + $prefixPatternGeneric = preg_quote($versionPrefix, '/'); + $versionPattern = '/(' . $prefixPatternGeneric . ')\d{2}\.\d{2}\.\d{2}/m'; + } else { + $versionPattern = '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m'; + } $directory = new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS); $filter = new RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) use ($excludeDirs) { if ($current->isDir() && in_array($current->getFilename(), $excludeDirs, true)) { diff --git a/cli/version_check.php b/cli/version_check.php index 5ac4f98..35db0da 100644 --- a/cli/version_check.php +++ b/cli/version_check.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/version_check.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Validate version consistency across README, manifests, and sub-packages */ diff --git a/cli/version_read.php b/cli/version_read.php index 4971a26..22a2fde 100644 --- a/cli/version_read.php +++ b/cli/version_read.php @@ -34,6 +34,7 @@ class VersionReadCli extends CliFramework // -- 1. Read from .mokogitea/manifest.xml (canonical source) -- $mokoVersion = null; + $versionPrefix = ''; $mokoManifest = "{$root}/.mokogitea/manifest.xml"; if (file_exists($mokoManifest)) { $xml = @simplexml_load_file($mokoManifest); @@ -42,6 +43,12 @@ class VersionReadCli extends CliFramework if (preg_match('/^\d{2}\.\d{2}\.\d{2}((?:-(?:dev|alpha|beta|rc))+)?$/', $v)) { $mokoVersion = $v; } + // Read version_prefix (supports both nested and flat structure) + $prefix = (string)($xml->identity->version_prefix ?? ''); + if ($prefix === '') { + $prefix = (string)($xml->version_prefix ?? ''); + } + $versionPrefix = $prefix; } } @@ -56,7 +63,14 @@ class VersionReadCli extends CliFramework $readme = "{$root}/README.md"; if (file_exists($readme)) { $content = file_get_contents($readme); - if (preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { + if (!empty($versionPrefix)) { + // Prefix-aware: search for prefix followed by version + $prefixPattern = preg_quote($versionPrefix, '/'); + if (preg_match('/' . $prefixPattern . '(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { + $readmeVersion = $m[1]; + } + } + if ($readmeVersion === null && preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { $readmeVersion = $m[1]; } } @@ -75,10 +89,22 @@ class VersionReadCli extends CliFramework if (strpos($xmlContent, '') === false) { continue; } + if (!empty($versionPrefix)) { + // Prefix-aware: look for prefix + XX.YY.ZZ + $prefixPattern = preg_quote($versionPrefix, '#'); + if (preg_match('#' . $prefixPattern . '(\d{2}\.\d{2}\.\d{2})#', $xmlContent, $xm)) { + $candidate = $xm[1]; + $currentBase = $manifestVersion ? preg_replace('/(-(?:dev|alpha|beta|rc))+$/', '', $manifestVersion) : null; + if ($currentBase === null || version_compare($candidate, $currentBase, '>')) { + $manifestVersion = $candidate; + } + continue; + } + } if (preg_match('#(\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?)#', $xmlContent, $xm)) { $candidate = $xm[1]; - $candidateBase = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $candidate); - $currentBase = $manifestVersion ? preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $manifestVersion) : null; + $candidateBase = preg_replace('/(-(?:dev|alpha|beta|rc))+$/', '', $candidate); + $currentBase = $manifestVersion ? preg_replace('/(-(?:dev|alpha|beta|rc))+$/', '', $manifestVersion) : null; if ($currentBase === null || version_compare($candidateBase, $currentBase, '>')) { $manifestVersion = $candidate; } diff --git a/cli/wiki_sync.php b/cli/wiki_sync.php index ae81267..4d156e0 100644 --- a/cli/wiki_sync.php +++ b/cli/wiki_sync.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/wiki_sync.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Sync select wiki pages from moko-platform to all template repos */ diff --git a/cli/workflow_sync.php b/cli/workflow_sync.php new file mode 100644 index 0000000..c02ddb4 --- /dev/null +++ b/cli/workflow_sync.php @@ -0,0 +1,646 @@ +#!/usr/bin/env php + + * + * 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/workflow_sync.php + * VERSION: 09.25.02 + * BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\CliFramework; + +class WorkflowSyncCli extends CliFramework +{ + private const PLATFORM_TEMPLATES = [ + 'joomla' => 'Template-Joomla', + 'dolibarr' => 'Template-Dolibarr', + 'go' => 'Template-Go', + 'mcp' => 'Template-MCP', + 'platform' => 'Template-Generic', + 'generic' => 'Template-Generic', + ]; + + private const DEFAULT_TEMPLATE = 'Template-Generic'; + private const GENERIC_TEMPLATE = 'Template-Generic'; + + private int $updated = 0; + private int $created = 0; + private int $skipped = 0; + private int $errors = 0; + + protected function configure(): void + { + $this->setDescription('Sync workflows from Generic → platform templates → live repos based on manifest.platform'); + $this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech'); + $this->addArgument('--token', 'Gitea API token', ''); + $this->addArgument('--org', 'Target organization', ''); + $this->addArgument('--branch', 'Target branch (default: main)', 'main'); + $this->addArgument('--phase', 'Phase to run: all, templates, repos (default: all)', 'all'); + $this->addArgument('--platform-filter', 'Only sync repos matching this platform', ''); + } + + protected function run(): int + { + $giteaUrl = rtrim($this->getArgument('--gitea-url'), '/'); + $token = $this->getArgument('--token'); + $org = $this->getArgument('--org'); + $branch = $this->getArgument('--branch'); + $phase = $this->getArgument('--phase'); + $platformFilter = $this->getArgument('--platform-filter'); + + if ($token === '') { + $this->log('ERROR', '--token is required.'); + return 1; + } + + if ($org === '') { + $this->log('ERROR', '--org is required.'); + return 1; + } + + if (!in_array($phase, ['all', 'templates', 'repos'], true)) { + $this->log('ERROR', "--phase must be one of: all, templates, repos (got: {$phase})"); + return 1; + } + + $this->log('INFO', "Workflow Sync — org: {$org}, branch: {$branch}, phase: {$phase}"); + + if ($platformFilter !== '') { + $this->log('INFO', "Platform filter: {$platformFilter}"); + } + + if ($this->dryRun) { + $this->log('INFO', '[DRY RUN] No changes will be made.'); + } + + echo "\n"; + + // Phase 1: Sync Generic → Platform Templates + if ($phase === 'all' || $phase === 'templates') { + $result = $this->syncGenericToTemplates($giteaUrl, $token, $org, $branch, $platformFilter); + + if ($result !== 0) { + return $result; + } + } + + // Phase 2: Sync Platform Templates → Live Repos + if ($phase === 'all' || $phase === 'repos') { + $result = $this->syncTemplatesToRepos($giteaUrl, $token, $org, $branch, $platformFilter); + + if ($result !== 0) { + return $result; + } + } + + echo "\n"; + $this->log('INFO', "Done: {$this->created} created, {$this->updated} updated, " + . "{$this->skipped} skipped, {$this->errors} error(s)."); + + return $this->errors > 0 ? 1 : 0; + } + + /** + * Phase 1: Push all Generic workflows to each platform template repo. + * Skips platform-specific overrides (files that exist in the platform template but NOT in Generic). + */ + private function syncGenericToTemplates( + string $giteaUrl, + string $token, + string $org, + string $branch, + string $platformFilter + ): int { + $this->log('INFO', '=== Phase 1: Sync Generic → Platform Templates ==='); + echo "\n"; + + // Get all workflow files from Template-Generic + $genericWorkflows = $this->listWorkflows($giteaUrl, $token, $org, self::GENERIC_TEMPLATE, $branch); + + if ($genericWorkflows === null) { + $this->log('ERROR', 'Could not list workflows from ' . self::GENERIC_TEMPLATE); + return 1; + } + + if (count($genericWorkflows) === 0) { + $this->log('WARN', 'No workflows found in ' . self::GENERIC_TEMPLATE); + return 0; + } + + $this->log('INFO', 'Found ' . count($genericWorkflows) . ' workflow(s) in ' . self::GENERIC_TEMPLATE); + echo "\n"; + + // Get unique platform templates (exclude Generic itself) + $platformTemplates = array_unique(array_filter( + array_values(self::PLATFORM_TEMPLATES), + fn(string $t) => $t !== self::GENERIC_TEMPLATE + )); + + // If platform-filter is set, only sync to the matching template + if ($platformFilter !== '') { + $targetTemplate = self::PLATFORM_TEMPLATES[$platformFilter] ?? null; + + if ($targetTemplate === null || $targetTemplate === self::GENERIC_TEMPLATE) { + $this->log('INFO', "Platform filter '{$platformFilter}' does not map to a non-generic template, skipping Phase 1."); + return 0; + } + + $platformTemplates = [$targetTemplate]; + } + + fprintf(STDERR, "%-45s | %s\n", 'Template / File', 'Status'); + fprintf(STDERR, "%s\n", str_repeat('-', 70)); + + foreach ($platformTemplates as $templateRepo) { + foreach ($genericWorkflows as $workflow) { + $filename = $workflow['name']; + $destPath = '.mokogitea/workflows/' . $filename; + $label = "{$templateRepo}/{$filename}"; + + // Get file content from Generic + $sourceContent = $this->getFileContent( + $giteaUrl, $token, $org, + self::GENERIC_TEMPLATE, $destPath, $branch + ); + + if ($sourceContent === null) { + fprintf(STDERR, "%-45s | %s\n", $label, 'ERROR (read source)'); + $this->errors++; + continue; + } + + $commitMsg = "chore: sync {$filename} from " . self::GENERIC_TEMPLATE . " [skip ci]"; + + $this->pushFile( + $giteaUrl, $token, $org, $templateRepo, + $destPath, $sourceContent, $branch, $commitMsg, $label + ); + } + } + + echo "\n"; + return 0; + } + + /** + * Phase 2: Sync platform template workflows to live repos based on manifest.platform. + */ + private function syncTemplatesToRepos( + string $giteaUrl, + string $token, + string $org, + string $branch, + string $platformFilter + ): int { + $this->log('INFO', '=== Phase 2: Sync Platform Templates → Live Repos ==='); + echo "\n"; + + $repos = $this->fetchOrgRepos($giteaUrl, $token, $org); + + if ($repos === null) { + return 1; + } + + $this->log('INFO', 'Found ' . count($repos) . " repo(s) in \"{$org}\"."); + echo "\n"; + + fprintf(STDERR, "%-45s | %s\n", 'Repo / File', 'Status'); + fprintf(STDERR, "%s\n", str_repeat('-', 70)); + + // Cache template workflows to avoid repeated API calls + $templateWorkflowCache = []; + + foreach ($repos as $repoFullName) { + [, $repoName] = explode('/', $repoFullName, 2); + + // Skip template repos + if (str_starts_with($repoName, 'Template-')) { + continue; + } + + // Read manifest.platform + $platform = $this->getRepoPlatform($giteaUrl, $token, $org, $repoName, $branch); + + // Apply platform filter + if ($platformFilter !== '' && $platform !== $platformFilter) { + continue; + } + + // Resolve template + $templateRepo = self::PLATFORM_TEMPLATES[$platform] ?? self::DEFAULT_TEMPLATE; + + // Get workflows from the template (cached) + if (!isset($templateWorkflowCache[$templateRepo])) { + $workflows = $this->listWorkflows($giteaUrl, $token, $org, $templateRepo, $branch); + + if ($workflows === null) { + $this->log('WARN', "Could not list workflows from {$templateRepo}, falling back to " . self::GENERIC_TEMPLATE); + $workflows = $this->listWorkflows($giteaUrl, $token, $org, self::GENERIC_TEMPLATE, $branch); + } + + $templateWorkflowCache[$templateRepo] = $workflows ?? []; + } + + $workflows = $templateWorkflowCache[$templateRepo]; + + if (count($workflows) === 0) { + continue; + } + + foreach ($workflows as $workflow) { + $filename = $workflow['name']; + $destPath = '.mokogitea/workflows/' . $filename; + $label = "{$repoFullName}/{$filename}"; + + // Get source content from template + $sourceContent = $this->getFileContent( + $giteaUrl, $token, $org, + $templateRepo, $destPath, $branch + ); + + if ($sourceContent === null) { + fprintf(STDERR, "%-45s | %s\n", $label, 'ERROR (read source)'); + $this->errors++; + continue; + } + + $commitMsg = "chore: sync {$filename} from {$templateRepo} [skip ci]"; + + $this->pushFile( + $giteaUrl, $token, $org, $repoName, + $destPath, $sourceContent, $branch, $commitMsg, $label + ); + } + } + + echo "\n"; + return 0; + } + + /** + * Push a file to a repo — create or update, skip if identical. + */ + private function pushFile( + string $giteaUrl, + string $token, + string $org, + string $repoName, + string $destPath, + string $localContent, + string $branch, + string $commitMsg, + string $label + ): void { + $existing = $this->apiRequest( + $giteaUrl, + $token, + 'GET', + "/api/v1/repos/{$org}/{$repoName}/contents/" + . "{$destPath}?ref={$branch}" + ); + + $encodedContent = base64_encode($localContent); + + if ($existing['code'] === 200) { + $data = json_decode($existing['body'], true); + $remoteSha = $data['sha'] ?? ''; + $remoteContent = base64_decode($data['content'] ?? ''); + + if ($remoteContent === $localContent) { + fprintf(STDERR, "%-45s | %s\n", $label, 'IDENTICAL (skipped)'); + $this->skipped++; + return; + } + + if ($this->dryRun) { + fprintf(STDERR, "%-45s | %s\n", $label, 'WOULD UPDATE'); + $this->updated++; + return; + } + + $payload = json_encode([ + 'content' => $encodedContent, + 'sha' => $remoteSha, + 'message' => $commitMsg, + 'branch' => $branch, + ]); + + $response = $this->apiRequest( + $giteaUrl, + $token, + 'PUT', + "/api/v1/repos/{$org}/{$repoName}/contents/" . $destPath, + $payload + ); + + if ($response['code'] === 200) { + fprintf(STDERR, "%-45s | %s\n", $label, 'UPDATED'); + $this->updated++; + } else { + fprintf(STDERR, "%-45s | %s\n", $label, "ERROR (HTTP {$response['code']})"); + $this->errors++; + } + } elseif ($existing['code'] === 404) { + if ($this->dryRun) { + fprintf(STDERR, "%-45s | %s\n", $label, 'WOULD CREATE'); + $this->created++; + return; + } + + $payload = json_encode([ + 'content' => $encodedContent, + 'message' => $commitMsg, + 'branch' => $branch, + ]); + + $response = $this->apiRequest( + $giteaUrl, + $token, + 'POST', + "/api/v1/repos/{$org}/{$repoName}/contents/" . $destPath, + $payload + ); + + if ($response['code'] === 201) { + fprintf(STDERR, "%-45s | %s\n", $label, 'CREATED'); + $this->created++; + } else { + fprintf(STDERR, "%-45s | %s\n", $label, "ERROR (HTTP {$response['code']})"); + $this->errors++; + } + } else { + fprintf(STDERR, "%-45s | %s\n", $label, "ERROR (HTTP {$existing['code']})"); + $this->errors++; + } + } + + /** + * List workflow files in a repo's .mokogitea/workflows/ directory. + */ + private function listWorkflows( + string $giteaUrl, + string $token, + string $org, + string $repoName, + string $branch + ): ?array { + $response = $this->apiRequest( + $giteaUrl, + $token, + 'GET', + "/api/v1/repos/{$org}/{$repoName}/contents/.mokogitea/workflows?ref={$branch}" + ); + + if ($response['code'] !== 200) { + return null; + } + + $data = json_decode($response['body'], true); + + if (!is_array($data)) { + return null; + } + + // Filter to only files (not directories) + return array_values(array_filter($data, fn($item) => ($item['type'] ?? '') === 'file')); + } + + /** + * Get file content from a repo as a raw string. + */ + private function getFileContent( + string $giteaUrl, + string $token, + string $org, + string $repoName, + string $filePath, + string $branch + ): ?string { + $response = $this->apiRequest( + $giteaUrl, + $token, + 'GET', + "/api/v1/repos/{$org}/{$repoName}/contents/{$filePath}?ref={$branch}" + ); + + if ($response['code'] !== 200) { + return null; + } + + $data = json_decode($response['body'], true); + + if (!is_array($data) || !isset($data['content'])) { + return null; + } + + return base64_decode($data['content']); + } + + /** + * Read a repo's manifest.xml and extract the platform value. + * Returns 'generic' if the manifest is missing or has no platform field. + */ + private function getRepoPlatform( + string $giteaUrl, + string $token, + string $org, + string $repoName, + string $branch + ): string { + $response = $this->apiRequest( + $giteaUrl, + $token, + 'GET', + "/api/v1/repos/{$org}/{$repoName}/contents/.mokogitea/manifest.xml?ref={$branch}" + ); + + if ($response['code'] !== 200) { + return 'generic'; + } + + $data = json_decode($response['body'], true); + + if (!is_array($data) || !isset($data['content'])) { + return 'generic'; + } + + $xmlContent = base64_decode($data['content']); + + if ($xmlContent === false || $xmlContent === '') { + return 'generic'; + } + + // Suppress XML warnings for malformed manifests + $previous = libxml_use_internal_errors(true); + $xml = simplexml_load_string($xmlContent); + libxml_use_internal_errors($previous); + + if ($xml === false) { + return 'generic'; + } + + // Try (standard location) + $platform = ''; + + // Register namespace if present + $namespaces = $xml->getNamespaces(true); + + if (!empty($namespaces)) { + $ns = reset($namespaces); + $xml->registerXPathNamespace('mp', $ns); + + $nodes = $xml->xpath('//mp:governance/mp:platform'); + + if (!empty($nodes)) { + $platform = trim((string) $nodes[0]); + } + + // Fallback: + if ($platform === '') { + $nodes = $xml->xpath('//mp:identity/mp:platform'); + + if (!empty($nodes)) { + $platform = trim((string) $nodes[0]); + } + } + + // Fallback: top-level + if ($platform === '') { + $nodes = $xml->xpath('//mp:platform'); + + if (!empty($nodes)) { + $platform = trim((string) $nodes[0]); + } + } + } else { + // No namespace + if (isset($xml->governance->platform)) { + $platform = trim((string) $xml->governance->platform); + } elseif (isset($xml->identity->platform)) { + $platform = trim((string) $xml->identity->platform); + } elseif (isset($xml->platform)) { + $platform = trim((string) $xml->platform); + } + } + + if ($platform === '') { + return 'generic'; + } + + return strtolower($platform); + } + + /** + * Fetch all non-archived repos in an org (paginated). + */ + private function fetchOrgRepos(string $giteaUrl, string $token, string $org): ?array + { + $this->log('INFO', "Fetching repos from org: {$org}"); + + $page = 1; + $repos = []; + + while (true) { + $response = $this->apiRequest( + $giteaUrl, + $token, + 'GET', + "/api/v1/orgs/{$org}/repos?" + . "limit=50&page={$page}" + ); + + if ($response['code'] < 200 || $response['code'] >= 300) { + if ($page === 1) { + $this->log('ERROR', "Could not fetch repos " + . "(HTTP {$response['code']})."); + return null; + } + + break; + } + + $data = json_decode($response['body'], true); + + if (!is_array($data) || count($data) === 0) { + break; + } + + foreach ($data as $repo) { + if (!empty($repo['archived'])) { + continue; + } + + $fullName = $repo['full_name'] ?? ''; + + if ($fullName !== '') { + $repos[] = $fullName; + } + } + + $page++; + } + + return $repos; + } + + /** + * Make an HTTP request to the Gitea API. + */ + private function apiRequest( + string $giteaUrl, + string $token, + string $method, + string $endpoint, + ?string $body = null + ): array { + $url = $giteaUrl . $endpoint; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Accept: application/json', + "Authorization: token {$token}", + ]); + + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $responseBody = curl_exec($ch); + $httpCode = (int) curl_getinfo( + $ch, + CURLINFO_HTTP_CODE + ); + + if (curl_errno($ch)) { + $error = curl_error($ch); + curl_close($ch); + + return [ + 'code' => 0, + 'body' => "cURL error: {$error}", + ]; + } + + curl_close($ch); + + return ['code' => $httpCode, 'body' => $responseBody]; + } +} + +$app = new WorkflowSyncCli(); +exit($app->execute()); diff --git a/deploy/backup-before-deploy.php b/deploy/backup-before-deploy.php index 7ee4ea4..4c8fb07 100644 --- a/deploy/backup-before-deploy.php +++ b/deploy/backup-before-deploy.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/backup-before-deploy.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Snapshot Joomla directories before deployment for rollback capability */ diff --git a/deploy/deploy-dolibarr.php b/deploy/deploy-dolibarr.php index 4c6fd74..9e0f256 100644 --- a/deploy/deploy-dolibarr.php +++ b/deploy/deploy-dolibarr.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/deploy-dolibarr.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync */ diff --git a/deploy/health-check.php b/deploy/health-check.php index d3c2138..0fd771d 100644 --- a/deploy/health-check.php +++ b/deploy/health-check.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/health-check.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Post-deploy health check — verify a Joomla site is responding correctly */ diff --git a/deploy/rollback-joomla.php b/deploy/rollback-joomla.php index 4ec27f6..6250b70 100644 --- a/deploy/rollback-joomla.php +++ b/deploy/rollback-joomla.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/rollback-joomla.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot */ diff --git a/deploy/sync-joomla.php b/deploy/sync-joomla.php index ad63ffd..804acd7 100644 --- a/deploy/sync-joomla.php +++ b/deploy/sync-joomla.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/sync-joomla.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Sync Joomla site directories between two servers via rsync over SSH */ diff --git a/tests/Unit/VersionBumpTest.php b/tests/Unit/VersionBumpTest.php index 27832a0..40e55e2 100644 --- a/tests/Unit/VersionBumpTest.php +++ b/tests/Unit/VersionBumpTest.php @@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "\nSome content\n" + "\nSome content\n" ); $this->execute(); diff --git a/tests/Unit/VersionReadTest.php b/tests/Unit/VersionReadTest.php index a3d9d1c..f0d7d22 100644 --- a/tests/Unit/VersionReadTest.php +++ b/tests/Unit/VersionReadTest.php @@ -34,7 +34,7 @@ class VersionReadTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "# Test\n\n" + "# Test\n\n" ); $this->assertSame('02.03.04', trim($this->runScript())); @@ -68,7 +68,7 @@ class VersionReadTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "\n" + "\n" ); mkdir("{$this->tmpDir}/src", 0755, true); file_put_contents( diff --git a/validate/check_file_integrity.php b/validate/check_file_integrity.php index 08282e7..bf37403 100644 --- a/validate/check_file_integrity.php +++ b/validate/check_file_integrity.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /validate/check_file_integrity.php - * VERSION: 09.25.00 + * VERSION: 09.25.02 * BRIEF: Compare deployed files on a remote server against the local repository to detect drift */