1 Commits

Author SHA1 Message Date
gitea-actions[bot] 082c01fc46 chore(release): build 09.24.00-rc [skip ci] 2026-06-04 22:59:27 +00:00
736 changed files with 5055 additions and 82505 deletions
-12
View File
@@ -1,12 +0,0 @@
[submodule "templates/repos/Template-Client"]
path = templates/repos/Template-Client
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Client.git
[submodule "templates/repos/Template-Generic"]
path = templates/repos/Template-Generic
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Generic.git
[submodule "templates/repos/Template-Joomla"]
path = templates/repos/Template-Joomla
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla.git
[submodule "templates/repos/Template-MCP"]
path = templates/repos/Template-MCP
url = https://git.mokoconsulting.tech/MokoConsulting/Template-MCP.git
-76
View File
@@ -1,76 +0,0 @@
# mokocli
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** | [mokocli Wiki](https://git.mokoconsulting.tech/MokoConsulting/mokocli/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 `MokoCli\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**: [MokoCli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)
+2 -2
View File
@@ -7,8 +7,8 @@ contact_links:
- name: 💬 Ask a Question - name: 💬 Ask a Question
url: https://mokoconsulting.tech/ url: https://mokoconsulting.tech/
about: Get help or ask questions through our website about: Get help or ask questions through our website
- name: 📚 mokocli Documentation - name: 📚 moko-platform Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/mokocli url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
about: View our coding standards and best practices about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability - name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
+1 -1
View File
@@ -42,7 +42,7 @@ Suggested text here
<!-- Add any other context, screenshots, or references --> <!-- Add any other context, screenshots, or references -->
## Standards Alignment ## Standards Alignment
- [ ] Follows mokocli documentation guidelines - [ ] Follows moko-platform documentation guidelines
- [ ] Uses en_US/en_GB localization - [ ] Uses en_US/en_GB localization
- [ ] Includes proper SPDX headers where applicable - [ ] Includes proper SPDX headers where applicable
+1 -1
View File
@@ -37,7 +37,7 @@ If you have ideas about how this could be implemented, share them here:
Add any other context, mockups, or screenshots about the feature request here. Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards ## Relevant Standards
Does this relate to any standards in [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli)? Does this relate to any standards in [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)?
- [ ] Accessibility (WCAG 2.1 AA) - [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB) - [ ] Localization (en_US/en_GB)
- [ ] Security best practices - [ ] Security best practices
+1 -1
View File
@@ -35,7 +35,7 @@ Use this template only for:
<!-- Describe how this could be addressed --> <!-- Describe how this could be addressed -->
## Standards Reference ## Standards Reference
Does this relate to security standards in [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli)? Does this relate to security standards in [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)?
- [ ] SPDX license identifiers - [ ] SPDX license identifiers
- [ ] Secret management - [ ] Secret management
- [ ] Dependency security - [ ] Dependency security
+6 -6
View File
@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/branch-protection.yml # PATH: /.gitea/workflows/branch-protection.yml
# BRIEF: Apply standardised branch protection rules to all governed repositories # BRIEF: Apply standardised branch protection rules to all governed repositories
# #
@@ -57,13 +57,13 @@ jobs:
- name: Determine target repos - name: Determine target repos
id: repos id: repos
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: | run: |
API="${GITEA_URL}/api/v1" API="${GITEA_URL}/api/v1"
# Platform/standards/infra repos to exclude # Platform/standards/infra repos to exclude
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokocli MokoTesting" EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate" EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then if [ -n "${{ inputs.repos }}" ]; then
# User-specified repos # User-specified repos
@@ -105,7 +105,7 @@ jobs:
- name: Apply protection rules - name: Apply protection rules
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
DRY_RUN: ${{ inputs.dry_run || 'false' }} DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: | run: |
API="${GITEA_URL}/api/v1" API="${GITEA_URL}/api/v1"
+5 -5
View File
@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/bulk-repo-sync.yml # PATH: /.gitea/workflows/bulk-repo-sync.yml
# BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos # BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos
@@ -84,8 +84,8 @@ jobs:
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}" echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
GH_TOKEN: ${{ secrets.GH_PAT }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_PLATFORM: gitea GIT_PLATFORM: gitea
GITEA_URL: https://git.mokoconsulting.tech GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting GITEA_ORG: MokoConsulting
@@ -112,7 +112,7 @@ jobs:
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)" bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
fi fi
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
GITEA_URL: https://git.mokoconsulting.tech GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting GITEA_ORG: MokoConsulting
+3 -3
View File
@@ -2,9 +2,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: mokocli.CI # DEFGROUP: moko-platform.CI
# INGROUP: mokocli # INGROUP: moko-platform
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/pr-branch-check.yml # PATH: /.gitea/workflows/pr-branch-check.yml
# BRIEF: PR branch merge policy enforcement # BRIEF: PR branch merge policy enforcement
# #
+6 -6
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/renovate.yml # PATH: /.gitea/workflows/renovate.yml
# BRIEF: Run Renovate Bot across all governed repos for dependency updates # BRIEF: Run Renovate Bot across all governed repos for dependency updates
# #
@@ -57,12 +57,12 @@ jobs:
- name: Determine target repos - name: Determine target repos
id: repos id: repos
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: | run: |
API="${GITEA_URL}/api/v1" API="${GITEA_URL}/api/v1"
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokocli MokoTesting" EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate" EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then if [ -n "${{ inputs.repos }}" ]; then
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ') REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
@@ -107,7 +107,7 @@ jobs:
- name: Run Renovate - name: Run Renovate
if: steps.repos.outputs.repo_list != '' if: steps.repos.outputs.repo_list != ''
env: env:
RENOVATE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} RENOVATE_TOKEN: ${{ secrets.GA_TOKEN }}
RENOVATE_PLATFORM: gitea RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1 RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>' RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
+3 -3
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Maintenance # INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/sync-wikis.yml # PATH: /.gitea/workflows/sync-wikis.yml
# BRIEF: Daily sync of all Gitea wikis to consolidated GitHub wiki repo # BRIEF: Daily sync of all Gitea wikis to consolidated GitHub wiki repo
@@ -31,7 +31,7 @@ jobs:
- name: Sync all wikis - name: Sync all wikis
env: env:
GH_TOKEN: ${{ secrets.GH_PAT }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: | run: |
if [ -z "$GH_TOKEN" ]; then if [ -z "$GH_TOKEN" ]; then
+66 -66
View File
@@ -1,66 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml # PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00 # VERSION: 09.23.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) # BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump" name: "Universal: Auto Version Bump"
on: on:
push: push:
branches: branches:
- dev - dev
- rc - rc
- 'feature/**' - 'feature/**'
- 'patch/**' - 'patch/**'
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions: permissions:
contents: write contents: write
jobs: jobs:
bump: bump:
name: Version Bump name: Version Bump
runs-on: release runs-on: release
if: >- if: >-
!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') && !contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request') !startsWith(github.event.head_commit.message, 'Merge pull request')
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup mokocli tools - name: Setup moko-platform tools
run: | run: |
if ! command -v composer &> /dev/null; then 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 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 fi
if [ -d "/opt/mokocli/cli" ]; then if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else else
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/mokocli.git" \ "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/mokocli /tmp/moko-platform-api
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi fi
- name: Bump version - name: Bump version
run: | run: |
php ${MOKO_CLI}/version_auto_bump.php \ php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \ --path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+285 -421
View File
@@ -1,421 +1,285 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +========================================================================+ # +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+ # +========================================================================+
# | | # | |
# | 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, type-prefixed packages | # | joomla: XML manifest, updates.xml, 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 |
# | | # | |
# +========================================================================+ # +========================================================================+
name: "Universal: Build & Release" name: "Universal: Build & Release"
on: on:
pull_request: pull_request:
types: [opened, closed] types: [opened, closed]
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
action: action:
description: 'Action to perform' description: 'Action to perform'
required: false required: false
type: choice type: choice
default: release default: release
options: options:
- release - release
- promote-rc - promote-rc
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions: permissions:
contents: write contents: write
jobs: jobs:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────── # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc: promote-rc:
name: Promote to RC name: Promote to RC
runs-on: release runs-on: release
if: >- if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) || (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup mokocli 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 [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then if ! command -v composer &> /dev/null; then
echo Using pre-installed /opt/mokocli 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/mokocli/cli >> $GITHUB_ENV fi
else # Always fetch latest CLI tools — never use stale cache from previous runs
echo Falling back to fresh clone rm -rf /tmp/moko-platform-api
if ! command -v composer > /dev/null 2>&1; then git clone --depth 1 --branch main --quiet \
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 "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
fi /tmp/moko-platform-api
rm -rf /tmp/mokocli cd /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git composer install --no-dev --no-interaction --quiet
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli - name: Rename branch to rc
composer install --no-dev --no-interaction --quiet run: |
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV php /tmp/moko-platform-api/cli/branch_rename.php \
fi --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
- name: Rename branch to rc --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
run: | --pr "${{ github.event.pull_request.number }}"
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - name: Checkout rc and configure git
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ run: |
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ git fetch origin rc
--pr "${{ github.event.pull_request.number }}" git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- name: Checkout rc and configure git git config --local user.name "gitea-actions[bot]"
run: | git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git fetch origin rc
git checkout rc - name: Publish RC release
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" run: |
git config --local user.name "gitea-actions[bot]" php /tmp/moko-platform-api/cli/release_publish.php \
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
- name: Publish RC release --skip-update-stream
run: |
php ${MOKO_CLI}/release_publish.php \ - name: Summary
--path . --stability rc --bump minor --branch rc \ if: always()
--token "${{ secrets.MOKOGITEA_TOKEN }}" run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- name: Update RC release notes from CHANGELOG.md echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" release:
name: Build & Release Pipeline
# Extract [Unreleased] section from changelog runs-on: release
NOTES="" if: >-
if [ -f "CHANGELOG.md" ]; then github.event.pull_request.merged == true ||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
fi
[ -z "$NOTES" ] && NOTES="Release candidate" steps:
- name: Checkout repository
# Find the RC release and update its body uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ with:
"${API_BASE}/releases/tags/release-candidate" \ token: ${{ secrets.MOKOGITEA_TOKEN }}
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) fetch-depth: 0
if [ -n "$RELEASE_ID" ]; then - name: Configure git for bot pushes
python3 -c " run: |
import json, urllib.request git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
body = open('/dev/stdin').read() git config --local user.name "gitea-actions[bot]"
payload = json.dumps({'body': body}).encode() git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}', - name: Check for merge conflict markers
data=payload, method='PATCH', run: |
headers={ CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
'Authorization': 'token ${TOKEN}', if [ -n "$CONFLICTS" ]; then
'Content-Type': 'application/json' echo "::error::Merge conflict markers found — aborting release"
}) echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
urllib.request.urlopen(req) echo '```' >> $GITHUB_STEP_SUMMARY
" <<< "$NOTES" echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo "RC release notes updated from CHANGELOG.md" echo '```' >> $GITHUB_STEP_SUMMARY
fi exit 1
fi
- name: Summary echo "No conflict markers found"
if: always()
run: | - name: Setup moko-platform tools
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY env:
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
# ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
release: run: |
name: Build & Release Pipeline # Ensure PHP + Composer are available
runs-on: release if ! command -v composer &> /dev/null; then
if: >- 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
github.event.pull_request.merged == true || fi
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') # Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
steps: git clone --depth 1 --branch main --quiet \
- name: Checkout repository "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 /tmp/moko-platform-api
with: cd /tmp/moko-platform-api
token: ${{ secrets.MOKOGITEA_TOKEN }} composer install --no-dev --no-interaction --quiet
fetch-depth: 0
- name: Configure git for bot pushes - name: "Publish stable release"
run: | run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" php /tmp/moko-platform-api/cli/release_publish.php \
git config --local user.name "gitea-actions[bot]" --path . --stability stable --bump minor --branch main \
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Check for merge conflict markers
run: | # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - name: "Step 9: Mirror release to GitHub"
if [ -n "$CONFLICTS" ]; then if: >-
echo "::error::Merge conflict markers found — aborting release" steps.version.outputs.skip != 'true' &&
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY secrets.GH_MIRROR_TOKEN != ''
echo '```' >> $GITHUB_STEP_SUMMARY continue-on-error: true
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY run: |
echo '```' >> $GITHUB_STEP_SUMMARY VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
exit 1 RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
fi GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
echo "No conflict markers found" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
- name: Setup mokocli tools --version "$VERSION" --tag "$RELEASE_TAG" \
env: --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting --branch main 2>&1 || true
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
echo Using pre-installed /opt/mokocli - name: "Step 10: Push main to GitHub mirror"
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV if: >-
else steps.version.outputs.skip != 'true' &&
echo Falling back to fresh clone secrets.GH_MIRROR_TOKEN != ''
if ! command -v composer > /dev/null 2>&1; then continue-on-error: true
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 run: |
fi GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
rm -rf /tmp/mokocli GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
cd /tmp/mokocli git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
composer install --no-dev --no-interaction --quiet git fetch origin main --depth=1
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV git push github origin/main:refs/heads/main --force 2>/dev/null \
fi && echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Detect platform"
id: platform - name: "Step 11: Delete rc branch and recreate dev from main"
run: | if: steps.version.outputs.skip != 'true'
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true continue-on-error: true
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- name: "Determine version bump level" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
id: bump
run: | # Delete rc branch (ephemeral — created by promote-rc)
# Fix/patch branches: version was already bumped by pre-release, just strip suffix curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
# Feature/dev branches: bump minor for the new stable release "${API_BASE}/branches/rc" 2>/dev/null \
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" && echo "Deleted rc branch" || echo "rc branch not found"
case "$HEAD_REF" in
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; # Delete dev branch
*) BUMP="minor" ;; curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
esac "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" # Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- name: "Publish stable release" -H "Content-Type: application/json" \
run: | "${API_BASE}/branches" \
BUMP_FLAG="" -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
fi
php ${MOKO_CLI}/release_publish.php \ - name: "Step 12: Create version branch from main"
--path . --stability stable ${BUMP_FLAG} --branch main \ if: steps.version.outputs.skip != 'true'
--token "${{ secrets.MOKOGITEA_TOKEN }}" continue-on-error: true
run: |
- name: "Read published version" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
id: version TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") BRANCH_NAME="version/${VERSION}"
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') MAIN_SHA=$(git rev-parse HEAD)
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" # Delete old version branch if it exists (same version re-release)
echo "tag=stable" >> "$GITHUB_OUTPUT" curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "branch=main" >> "$GITHUB_OUTPUT" # Create version/XX.YY.ZZ from main
echo "Published version: ${VERSION}" curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
- name: Update release notes and promote changelog echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# -- Dolibarr post-release: Reset dev version -----------------------------
# Get the stable release info (version and ID) - name: "Post-release: Reset dev version"
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ if: steps.version.outputs.skip != 'true'
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') continue-on-error: true
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) run: |
# Extract version from release name (e.g. "06.17.00" or "v06.17.00") API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION=$(python3 -c " php /tmp/moko-platform-api/cli/version_reset_dev.php \
import json, sys, re --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
r = json.load(sys.stdin) --branch dev --path . 2>&1 || true
name = r.get('name', '')
m = re.search(r'(\d+\.\d+\.\d+)', name) # -- Summary --------------------------------------------------------------
print(m.group(1) if m else '') - name: Pipeline Summary
" <<< "$RELEASE_JSON" 2>/dev/null || true) if: always()
run: |
# Extract [Unreleased] section from changelog VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
NOTES="" PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -f "CHANGELOG.md" ]; then if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
fi echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
[ -z "$NOTES" ] && NOTES="Stable release" elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
# Update release body via API else
if [ -n "$RELEASE_ID" ]; then echo "" >> $GITHUB_STEP_SUMMARY
python3 -c " echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
import json, urllib.request echo "" >> $GITHUB_STEP_SUMMARY
body = open('/dev/stdin').read() echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
payload = json.dumps({'body': body}).encode() echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
req = urllib.request.Request( echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
'${API_BASE}/releases/${RELEASE_ID}', echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
data=payload, method='PATCH', echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
headers={ echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
'Authorization': 'token ${TOKEN}', echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
'Content-Type': 'application/json' fi
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
python3 -c "
import sys
version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
git push origin main || true
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+3 -3
View File
@@ -4,10 +4,10 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.Universal # INGROUP: MokoPlatform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/branch-cleanup.yml # PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00 # VERSION: 09.23.00
# BRIEF: Delete feature branches after PR merge # BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup" name: "Branch Cleanup"
-191
View File
@@ -1,191 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.gitea/workflows/ci-generic.yml
# VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
name: "Generic: Project CI"
on:
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Lint & Validate ───────────────────────────────────────────────────
lint:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
php -v
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install PHP dependencies
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
fi
- name: Install Node.js dependencies
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "package.json" ]; then
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
fi
- name: PHP syntax check
if: steps.detect.outputs.has_php == 'true'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
echo "::error file=${file}::PHP syntax error"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -eq 0 ]; then
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
else
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: TypeScript/JavaScript lint
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "node_modules/.bin/eslint" ]; then
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
echo "::warning::ESLint config found but eslint not installed"
else
echo "No ESLint configured — skipping"
fi
- name: TypeScript compile check
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
fi
- name: PHPStan static analysis
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
fi
# ── Tests ─────────────────────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
- name: Run PHP tests
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "vendor/bin/phpunit" ]; then
vendor/bin/phpunit --testdox 2>&1
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
echo "::warning::PHPUnit config found but phpunit not installed"
else
echo "No PHPUnit configured — skipping"
fi
- name: Run Node.js tests
if: steps.detect.outputs.has_node == 'true'
run: |
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
npm test 2>&1
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "No test script in package.json — skipping"
fi
- name: Build check
run: |
if [ -f "Makefile" ]; then
make build 2>&1 || echo "::warning::Build failed or not configured"
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
npm run build 2>&1 || echo "::warning::Build failed"
fi
+11 -60
View File
@@ -4,18 +4,18 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.CI # INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/ci-platform.yml # PATH: /.gitea/workflows/ci-platform.yml
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: mokocli CI — the standards engine validates itself # BRIEF: moko-platform CI — the standards engine validates itself
# #
# +========================================================================+ # +========================================================================+
# | MOKO-PLATFORM CI | # | MOKO-PLATFORM CI |
# +========================================================================+ # +========================================================================+
# | | # | |
# | This is NOT a generic CI workflow. This is the self-validation | # | This is NOT a generic CI workflow. This is the self-validation |
# | pipeline for the central mokocli enterprise engine. | # | pipeline for the central moko-platform enterprise engine. |
# | | # | |
# | It dogfoods every tool the platform ships to governed repos: | # | It dogfoods every tool the platform ships to governed repos: |
# | | # | |
@@ -29,7 +29,7 @@
# | | # | |
# +========================================================================+ # +========================================================================+
name: "Platform: mokocli CI" name: "Platform: moko-platform CI"
on: on:
push: push:
@@ -41,7 +41,7 @@ on:
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- 'wiki/**' - 'wiki/**'
- '.mokogitea/ISSUE_TEMPLATE/**' - '.gitea/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/ source/ src/ deploy/ -name "*.php" -print0 2>/dev/null) done < <(find lib/ validate/ automation/ cli/ 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/ source/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null) done < <(find lib/ validate/ cli/ 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/ source/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null) done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
{ {
echo "### Secret Detection" echo "### Secret Detection"
@@ -412,16 +412,10 @@ jobs:
if: always() if: always()
steps: steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: Check gate results - name: Check gate results
run: | run: |
{ {
echo "# mokocli CI" echo "# moko-platform CI"
echo "" echo ""
echo "| Gate | Job | Status |" echo "| Gate | Job | Status |"
echo "|---|---|---|" echo "|---|---|---|"
@@ -443,46 +437,3 @@ jobs:
echo "::error::One or more CI gates failed" echo "::error::One or more CI gates failed"
exit 1 exit 1
fi fi
- name: "File issues for failed gates"
if: >-
always() &&
(needs.code-quality.result == 'failure' ||
needs.tests.result == 'failure' ||
needs.self-health.result == 'failure' ||
needs.governance.result == 'failure' ||
needs.templates.result == 'failure')
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
REPORTER="./automation/ci-issue-reporter.sh"
WF="Platform CI"
report_gate() {
local gate="$1" result="$2" details="$3"
if [ "$result" = "failure" ]; then
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
fi
}
report_gate "Code Quality" \
"${{ needs.code-quality.result }}" \
"PHPCS (PSR-12), PHPStan, or PHP syntax checks failed. Run \`composer check\` locally to reproduce."
report_gate "Unit Tests" \
"${{ needs.tests.result }}" \
"PHPUnit tests failed on one or more PHP versions (8.1, 8.2, 8.3). Run \`vendor/bin/phpunit --testdox\` locally."
report_gate "Self-Health" \
"${{ needs.self-health.result }}" \
"Self-health score fell below the 80% threshold. Run \`php bin/moko health -- --path .\` locally."
report_gate "Governance" \
"${{ needs.governance.result }}" \
"Governance checks failed (license headers, secrets, or version consistency). Check the CI run summary for specifics."
report_gate "Template Integrity" \
"${{ needs.templates.result }}" \
"Workflow or gitignore templates failed YAML validation or are missing required entries."
+10 -10
View File
@@ -4,10 +4,10 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.Maintenance # INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/cleanup.yml # PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.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
name: "Universal: Repository Cleanup" name: "Universal: Repository Cleanup"
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API # List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name') "${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0 DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main # Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}" echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true "${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
fi fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs - name: Clean old workflow runs
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs # Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \ "${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0 DELETED=0
for RUN_ID in $RUNS; do for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
done done
-76
View File
@@ -1,76 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
name: "Publish to Composer"
on:
push:
tags:
- 'v*'
- '[0-9]*.[0-9]*.[0-9]*'
release:
types: [published]
workflow_dispatch:
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
publish:
name: Publish Package
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip publish]')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /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
- name: Install dependencies
run: composer install --no-dev --no-interaction --prefer-dist --quiet
- name: Determine version
id: version
run: |
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
# Gitea Composer Registry — auto-publishes from tags
# The tag push itself registers the package at:
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
- name: Verify Gitea registry
run: |
echo "Gitea Composer registry auto-publishes from tags."
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
echo "Install: composer require mokoconsulting/mokocli"
# Packagist — notify of new version
- name: Notify Packagist
if: secrets.PACKAGIST_TOKEN != ''
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Notifying Packagist of version ${VERSION}..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
&& echo "Packagist notified" \
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
- name: Summary
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoCli tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+7 -3
View File
@@ -4,10 +4,10 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.Security # INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/gitleaks.yml.template # PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00 # VERSION: 09.23.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
# #
# +========================================================================+ # +========================================================================+
@@ -25,6 +25,10 @@
name: "Universal: Secret Scanning" name: "Universal: Secret Scanning"
on: on:
pull_request:
branches:
- main
- 'dev/**'
schedule: schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch: workflow_dispatch:
+3 -3
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: moko-platform.Automation
# VERSION: 09.38.00 # VERSION: 09.24.00
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
@@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: Create branch and comment - name: Create branch and comment
run: | run: |
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}" ISSUE_TITLE="${{ github.event.issue.title }}"
+3 -3
View File
@@ -4,10 +4,10 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.Notifications # INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/notify.yml # PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.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
name: "Universal: Notifications" name: "Universal: Notifications"
File diff suppressed because it is too large Load Diff
+33 -101
View File
@@ -4,26 +4,19 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # 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: 05.01.00 # VERSION: 09.23.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
push: pull_request:
types: [closed]
branches: branches:
- dev - dev
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
@@ -46,11 +39,11 @@ env:
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release runs-on: release
if: >- if: >-
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
github.event_name == 'push' (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps: steps:
- name: Checkout - name: Checkout
@@ -58,81 +51,56 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
- name: Setup mokocli 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: |
# Use pre-installed /opt/mokocli if available (updated by cron every 6h) if ! command -v composer &> /dev/null; then
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/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/mokocli
echo MOKO_CLI=/opt/mokocli/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/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/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
run: | run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
run: | run: |
# Auto-detect stability from branch name on push, or use input on dispatch STABILITY="${{ inputs.stability || 'development' }}"
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;; development) TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;; alpha) TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;; beta) TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; release-candidate) TAG="release-candidate" ;;
esac esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC # Bump version: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in case "$STABILITY" in
release-candidate) BUMP="minor" ;; release-candidate) php ${MOKO_CLI}/version_bump.php --path . --minor 2>/dev/null || true ;;
*) BUMP="patch" ;; *) php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true ;;
esac esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true # Set stability suffix and fix consistency
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
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml # Read final version with suffix
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -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"
@@ -157,12 +125,11 @@ 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}${SUFFIX} ===" echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
- name: Create release - name: Create release
id: release id: release
@@ -173,42 +140,7 @@ jobs:
php ${MOKO_CLI}/release_create.php \ php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \ --path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --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
-66
View File
@@ -1,66 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
+19 -3
View File
@@ -4,10 +4,10 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoCli.Security # INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/security-audit.yml # PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00 # VERSION: 09.23.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages # BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit" name: "Universal: Security Audit"
@@ -80,3 +80,19 @@ jobs:
-H "Priority: high" \ -H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \ -d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true "${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
@@ -1,103 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/sync-feature-versions.yml
# VERSION: 01.00.00
# BRIEF: Merge dev into open feature branches after version bumps
name: "Universal: Sync Feature Branch Versions"
on:
push:
branches:
- dev
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
sync:
name: Sync feature branches with dev
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'chore(version)')
steps:
- name: Checkout dev
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: dev
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Configure git
run: |
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"
- name: Merge dev into feature branches
run: |
echo "=== Syncing feature branches with dev ==="
# Fetch all remote branches
git fetch origin
# Find feature branches (feature/*, fix/*, patch/*, hotfix/*, bugfix/*, chore/*)
BRANCHES=$(git branch -r --list 'origin/feature/*' 'origin/fix/*' 'origin/patch/*' 'origin/hotfix/*' 'origin/bugfix/*' 'origin/chore/*' | sed 's|origin/||; s/^[[:space:]]*//')
if [ -z "$BRANCHES" ]; then
echo "No feature branches found — nothing to sync"
exit 0
fi
SYNCED=0
SKIPPED=0
FAILED=0
for BRANCH in $BRANCHES; do
echo ""
echo "--- ${BRANCH} ---"
# Skip branches that are already up to date with dev
if git merge-base --is-ancestor dev "origin/${BRANCH}" 2>/dev/null; then
echo "Already up to date"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Try to merge dev into the branch
git checkout "origin/${BRANCH}" -B "$BRANCH" 2>/dev/null
if git merge dev --no-edit -m "chore: merge dev into ${BRANCH} (version sync) [skip ci]" 2>/dev/null; then
git push origin "$BRANCH" 2>/dev/null
echo "Synced successfully"
SYNCED=$((SYNCED + 1))
else
git merge --abort 2>/dev/null || true
echo "Merge conflict — skipping (manual rebase needed)"
FAILED=$((FAILED + 1))
fi
done
# Return to dev
git checkout dev 2>/dev/null || true
echo ""
echo "=== Summary ==="
echo "Synced: ${SYNCED}"
echo "Already current: ${SKIPPED}"
echo "Conflicts (skipped): ${FAILED}"
if [ "$FAILED" -gt 0 ]; then
echo "::warning::${FAILED} branch(es) had merge conflicts and need manual rebase"
fi
+302
View File
@@ -0,0 +1,302 @@
# 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,73 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-MCP) PLATFORM="mcp" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install dependencies
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
ARGS="--token ${MOKOGITEA_TOKEN}"
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"metadata": { "metadata": {
"generated_at": "2026-03-10T19:51:42.238134Z", "generated_at": "2026-03-10T19:51:42.238134Z",
"repository": "MokoConsulting/mokocli", "repository": "MokoConsulting/moko-platform",
"version": "1.0.0" "version": "1.0.0"
}, },
"scripts": [ "scripts": [
+19 -8
View File
@@ -2,9 +2,9 @@
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION FILE INFORMATION
DEFGROUP: MokoCli.Root DEFGROUP: MokoStandards.Root
INGROUP: MokoCli INGROUP: MokoStandards
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /CHANGELOG.md PATH: /CHANGELOG.md
BRIEF: Release changelog BRIEF: Release changelog
--> -->
@@ -12,12 +12,23 @@ BRIEF: Release changelog
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
## [09.38.00] --- 2026-06-21
## [09.37.00] --- 2026-06-21 ## [09.24.00] --- 2026-06-04
## [09.37.00] --- 2026-06-21 ## [09.23] --- 2026-05-31
## [09.36.00] --- 2026-06-21 ## [09.22] --- 2026-05-31
## [09.36.00] --- 2026-06-21 ### Changed
- **refactor(cli):** migrate 64 legacy scripts to CliFramework (#235) — all tools in cli/, automation/, maintenance/, deploy/, release/ now extend CliFramework with free --help, --verbose, --quiet, --dry-run, --json, banners, and coloured logging
### Fixed
- fix: auto-detect org/repo in updates_xml_build from manifest and git remote
- fix: restore hyphen in version suffixes
- fix: release names use standardized format
- fix: remove lesser stream copies, each stream updates independently
- fix: sort updates.xml entries dev first, stable last
## [09.21] --- 2026-05-30
## [09.20] --- 2026-05-30
+102
View File
@@ -0,0 +1,102 @@
# 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
+161 -161
View File
@@ -1,161 +1,161 @@
# Contributing to Moko Consulting Projects # Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy. Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow ## Branching Workflow
``` ```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
``` ```
### Step by step ### Step by step
1. **Create a feature branch** from `dev`: 1. **Create a feature branch** from `dev`:
```bash ```bash
git checkout dev && git pull git checkout dev && git pull
git checkout -b feature/my-change git checkout -b feature/my-change
``` ```
2. **Work and commit** on your feature branch. Push to origin. 2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it. 3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`. 4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate) - This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded - An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage: 5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built - Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built - Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc` - When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`. 6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline: 7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`) - Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version) - Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages - Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions) - `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main` - `dev` branch recreated from `main`
### Branch summary ### Branch summary
| Branch | Purpose | Created by | | Branch | Purpose | Created by |
|--------|---------|-----------| |--------|---------|-----------|
| `feature/*` | New features and fixes | Developer | | `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release | | `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` | | `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` | | `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main | | `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only | | `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI | | `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches ### Protected branches
| Branch | Direct push | Merge via | | Branch | Direct push | Merge via |
|--------|------------|-----------| |--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only | | `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* | | `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR | | `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename | | `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename | | `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) | | `feature/*` | Open | N/A (source branch) |
## Version Policy ## Version Policy
### Format ### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded: All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes) - **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main) - **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches) - **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major. Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes ### Stability suffixes
Each branch appends a suffix to indicate stability: Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example | | Branch | Suffix | Example |
|--------|--------|---------| |--------|--------|---------|
| `main` | (none) | `02.09.00` | | `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` | | `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` | | `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` | | `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` | | `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` | | `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump ### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`: On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented 1. Patch version incremented
2. Stability suffix `-dev` applied 2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) 3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops 4. Commit created with `[skip ci]` to avoid loops
### Release version flow ### Release version flow
Version bumps happen at specific release events: Version bumps happen at specific release events:
| Event | Bump | Example | | Event | Bump | Example |
|-------|------|---------| |-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` | | Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` | | Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) | | RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` | | Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies ### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version: When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta` - **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc` - **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed). This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files ### Version files
The version tools update all files containing version stamps: The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source) - `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag) - Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern) - `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml` - `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label - Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched. Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards ## Code Standards
- **PHP**: PSR-12, tabs for indentation - **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header - **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo) - **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names - **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages ## Commit Messages
Use conventional commit format: Use conventional commit format:
``` ```
type(scope): short description type(scope): short description
Optional body with context. Optional body with context.
Authored-by: Moko Consulting Authored-by: Moko Consulting
``` ```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci` Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages: Special flags in commit messages:
- `[skip ci]` — skip all CI workflows - `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only - `[skip bump]` — skip auto version bump only
## Reporting Issues ## Reporting Issues
Use the repository's issue tracker with the appropriate template. Use the repository's issue tracker with the appropriate template.
--- ---
*Moko Consulting <hello@mokoconsulting.tech>* *Moko Consulting <hello@mokoconsulting.tech>*
+2 -2
View File
@@ -4,14 +4,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION FILE INFORMATION
DEFGROUP: MokoPlatform.Root DEFGROUP: MokoPlatform.Root
INGROUP: MokoPlatform INGROUP: MokoPlatform
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /PLUGIN_SCRIPTS.md PATH: /PLUGIN_SCRIPTS.md
BRIEF: Plugin system CLI documentation BRIEF: Plugin system CLI documentation
--> -->
# Plugin System CLI Scripts # Plugin System CLI Scripts
Command-line scripts for validating, health checking, and managing projects using the mokocli plugin system. Command-line scripts for validating, health checking, and managing projects using the moko-platform plugin system.
## Available Scripts ## Available Scripts
+6 -6
View File
@@ -4,20 +4,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION FILE INFORMATION
DEFGROUP: MokoPlatform.Root DEFGROUP: MokoPlatform.Root
INGROUP: MokoPlatform INGROUP: MokoPlatform
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /README.md PATH: /README.md
VERSION: 09.38.00 VERSION: 09.24.00
BRIEF: Project overview and documentation BRIEF: Project overview and documentation
--> -->
# mokocli Enterprise API # moko-platform Enterprise API
![Version](https://img.shields.io/badge/version-09.01.00-blue) ![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green) ![Version](https://img.shields.io/badge/version-09.01.00-blue) ![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green)
PHP implementation of mokocli — enterprise standards, automation framework, workflow templates, and bulk sync tooling. PHP implementation of moko-platform — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoCli-API) > **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API)
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoCli-API) *(read-only mirror)* > **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoStandards-API) *(read-only mirror)*
## What Lives Here ## What Lives Here
+1 -1
View File
@@ -4,7 +4,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION FILE INFORMATION
DEFGROUP: MokoPlatform.Index DEFGROUP: MokoPlatform.Index
INGROUP: MokoPlatform.Analysis INGROUP: MokoPlatform.Analysis
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /analysis/index.md PATH: /analysis/index.md
BRIEF: Analysis directory index BRIEF: Analysis directory index
--> -->
+4 -4
View File
@@ -11,7 +11,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform.Scripts * INGROUP: MokoPlatform.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/bulk_joomla_template.php * PATH: /automation/bulk_joomla_template.php
* BRIEF: Bulk scaffold and sync Joomla template repositories * BRIEF: Bulk scaffold and sync Joomla template repositories
* *
@@ -28,7 +28,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{ use MokoEnterprise\{
AuditLogger, AuditLogger,
CliFramework, CliFramework,
Config, Config,
@@ -42,7 +42,7 @@ use MokoCli\{
* *
* Provides three operations for Joomla template projects: * Provides three operations for Joomla template projects:
* --scaffold: Create a new template repository with the full directory structure * --scaffold: Create a new template repository with the full directory structure
* --sync: Push mokocli files to existing template repositories * --sync: Push moko-platform files to existing template repositories
* --list: List all repositories tagged as joomla-template * --list: List all repositories tagged as joomla-template
* *
* Works with both GitHub and Gitea via the PlatformAdapterFactory. * Works with both GitHub and Gitea via the PlatformAdapterFactory.
@@ -318,7 +318,7 @@ class BulkJoomlaTemplate extends CliFramework
$name, $name,
$path, $path,
$content, $content,
"chore: update {$path} from mokocli", "chore: update {$path} from moko-platform",
$existingSha, $existingSha,
$branch $branch
); );
+41 -41
View File
@@ -11,7 +11,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform.Scripts * INGROUP: MokoPlatform.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/bulk_sync.php * PATH: /automation/bulk_sync.php
* BRIEF: Enterprise-grade bulk repository synchronization * BRIEF: Enterprise-grade bulk repository synchronization
*/ */
@@ -21,7 +21,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{ use MokoEnterprise\{
ApiClient, ApiClient,
AuditLogger, AuditLogger,
CheckpointManager, CheckpointManager,
@@ -42,7 +42,7 @@ use MokoCli\{
/** /**
* Bulk Repository Synchronization Tool * Bulk Repository Synchronization Tool
* *
* Synchronizes mokocli files across multiple repositories using * Synchronizes moko-platform files across multiple repositories using
* the Enterprise library for robust, audited operations. * the Enterprise library for robust, audited operations.
*/ */
class BulkSync extends CliFramework class BulkSync extends CliFramework
@@ -95,7 +95,7 @@ class BulkSync extends CliFramework
*/ */
protected function run(): int protected function run(): int
{ {
$this->log("🚀 mokocli Bulk Synchronization v" . self::VERSION, 'INFO'); $this->log("🚀 moko-platform Bulk Synchronization v" . self::VERSION, 'INFO');
// Initialize enterprise components // Initialize enterprise components
if (!$this->initializeComponents()) { if (!$this->initializeComponents()) {
@@ -180,7 +180,7 @@ class BulkSync extends CliFramework
$results['health'] = $this->runHealthChecksAll($org, $repositories); $results['health'] = $this->runHealthChecksAll($org, $repositories);
} }
// Create/update tracking issue in mokocli // Create/update tracking issue in moko-platform
$this->createSyncIssue($org, $results); $this->createSyncIssue($org, $results);
// Create/update a failure issue when any repos failed // Create/update a failure issue when any repos failed
@@ -244,7 +244,7 @@ class BulkSync extends CliFramework
* Filter repositories based on include/exclude lists * Filter repositories based on include/exclude lists
*/ */
/** Repositories that are permanently excluded from bulk sync. */ /** Repositories that are permanently excluded from bulk sync. */
private const ALWAYS_EXCLUDE = ['mokocli', '.github-private']; private const ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
private function filterRepositories(array $repositories, array $include, array $exclude): array private function filterRepositories(array $repositories, array $include, array $exclude): array
{ {
@@ -426,7 +426,7 @@ class BulkSync extends CliFramework
$this->log("", 'ERROR'); $this->log("", 'ERROR');
$this->log("Required Implementation:", 'ERROR'); $this->log("Required Implementation:", 'ERROR');
$this->log(" 1. Clone/fetch target repository", 'ERROR'); $this->log(" 1. Clone/fetch target repository", 'ERROR');
$this->log(" 2. Apply file updates based on mokocli configuration", 'ERROR'); $this->log(" 2. Apply file updates based on moko-platform configuration", 'ERROR');
$this->log(" 3. Create pull request with changes", 'ERROR'); $this->log(" 3. Create pull request with changes", 'ERROR');
$this->log(" 4. Handle merge conflicts and validation", 'ERROR'); $this->log(" 4. Handle merge conflicts and validation", 'ERROR');
$this->log("", 'ERROR'); $this->log("", 'ERROR');
@@ -837,7 +837,7 @@ class BulkSync extends CliFramework
} }
/** /**
* Ensure all standard mokocli labels exist on a target repository. * Ensure all standard moko-platform labels exist on a target repository.
* *
* Fetches existing labels first (GET) and only POSTs the ones that are * Fetches existing labels first (GET) and only POSTs the ones that are
* missing. This avoids the 422 "already exists" responses that would * missing. This avoids the 422 "already exists" responses that would
@@ -872,7 +872,7 @@ class BulkSync extends CliFramework
// Workflow / Process // Workflow / Process
['automation', '8B4513', 'Automated processes or scripts'], ['automation', '8B4513', 'Automated processes or scripts'],
['mokocli', 'B60205', 'mokocli compliance'], ['moko-platform', 'B60205', 'moko-platform compliance'],
['needs-review', 'FBCA04', 'Awaiting code review'], ['needs-review', 'FBCA04', 'Awaiting code review'],
['work-in-progress', 'D93F0B', 'Work in progress, not ready for merge'], ['work-in-progress', 'D93F0B', 'Work in progress, not ready for merge'],
['breaking-change', 'D73A4A', 'Breaking API or functionality change'], ['breaking-change', 'D73A4A', 'Breaking API or functionality change'],
@@ -912,8 +912,8 @@ class BulkSync extends CliFramework
['health: poor', 'FF6B6B', 'Health score below 50'], ['health: poor', 'FF6B6B', 'Health score below 50'],
// Sync / Automation (used by bulk_sync, scan_drift, check_repo_health) // Sync / Automation (used by bulk_sync, scan_drift, check_repo_health)
['standards-update', 'B60205', 'mokocli sync update'], ['standards-update', 'B60205', 'moko-platform sync update'],
['standards-drift', 'FBCA04', 'Repository drifted from mokocli'], ['standards-drift', 'FBCA04', 'Repository drifted from moko-platform'],
['sync-report', '0075CA', 'Bulk sync run report'], ['sync-report', '0075CA', 'Bulk sync run report'],
['sync-failure', 'D73A4A', 'Bulk sync failure requiring attention'], ['sync-failure', 'D73A4A', 'Bulk sync failure requiring attention'],
['push-failure', 'D73A4A', 'File push failure requiring attention'], ['push-failure', 'D73A4A', 'File push failure requiring attention'],
@@ -925,10 +925,10 @@ class BulkSync extends CliFramework
['type: version', '0E8A16', 'Version-related change'], ['type: version', '0E8A16', 'Version-related change'],
]; ];
// Quick check: if the repo already has the 'mokocli' label, it was // Quick check: if the repo already has the 'moko-platform' label, it was
// provisioned previously — skip the expensive full label provisioning. // provisioned previously — skip the expensive full label provisioning.
try { try {
$probe = $this->api->get("/repos/{$org}/{$repo}/labels/mokocli"); $probe = $this->api->get("/repos/{$org}/{$repo}/labels/moko-platform");
if (!empty($probe['name'])) { if (!empty($probe['name'])) {
return; // already provisioned return; // already provisioned
} }
@@ -1024,7 +1024,7 @@ class BulkSync extends CliFramework
*/ */
private function updateOpenBranches(string $org, string $repo): void private function updateOpenBranches(string $org, string $repo): void
{ {
$syncBranchPrefix = 'chore/sync-mokocli-'; $syncBranchPrefix = 'chore/sync-moko-platform-';
try { try {
$defaultBranch = 'main'; $defaultBranch = 'main';
@@ -1055,7 +1055,7 @@ class BulkSync extends CliFramework
$this->api->post("/repos/{$org}/{$repo}/merges", [ $this->api->post("/repos/{$org}/{$repo}/merges", [
'base' => $branch, 'base' => $branch,
'head' => $defaultBranch, 'head' => $defaultBranch,
'commit_message' => "chore: merge {$defaultBranch} into {$branch} (mokocli sync)", 'commit_message' => "chore: merge {$defaultBranch} into {$branch} (moko-platform sync)",
]); ]);
$this->log(" 🔀 Merged {$defaultBranch}{$branch} (PR #{$prNum})", 'INFO'); $this->log(" 🔀 Merged {$defaultBranch}{$branch} (PR #{$prNum})", 'INFO');
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -1076,7 +1076,7 @@ class BulkSync extends CliFramework
/** /**
* Records which sync run touched the repo, the PR number, and the * Records which sync run touched the repo, the PR number, and the
* mokocli version that was applied — giving each repo a clear audit * moko-platform version that was applied — giving each repo a clear audit
* trail of what was changed and why. * trail of what was changed and why.
*/ */
/** /**
@@ -1119,16 +1119,16 @@ class BulkSync extends CliFramework
$minor = self::VERSION_MINOR; $minor = self::VERSION_MINOR;
$force = isset($this->options['force']) ? ' *(--force)*' : ''; $force = isset($this->options['force']) ? ' *(--force)*' : '';
$prLink = $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber); $prLink = $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber);
$source = $this->adapter->getRepoWebUrl($org, 'mokocli'); $source = $this->adapter->getRepoWebUrl($org, 'moko-platform');
$branchName = 'chore/sync-mokocli-v' . $minor; $branchName = 'chore/sync-moko-platform-v' . $minor;
$branchLink = $this->adapter->getBranchWebUrl($org, $repo, $branchName); $branchLink = $this->adapter->getBranchWebUrl($org, $repo, $branchName);
$title = "chore: mokocli v{$minor} sync tracking"; $title = "chore: moko-platform v{$minor} sync tracking";
$body = <<<MD $body = <<<MD
## mokocli Sync Applied ## moko-platform Sync Applied
A mokocli bulk sync run has updated files in this repository. A moko-platform bulk sync run has updated files in this repository.
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
@@ -1144,13 +1144,13 @@ class BulkSync extends CliFramework
Protected files (README, CHANGELOG, GOVERNANCE, etc.) were not overwritten. Protected files (README, CHANGELOG, GOVERNANCE, etc.) were not overwritten.
--- ---
*Updated automatically by [mokocli]({$source}) `bulk_sync.php`* *Updated automatically by [moko-platform]({$source}) `bulk_sync.php`*
MD; MD;
// Dedent heredoc // Dedent heredoc
$body = preg_replace('/^ /m', '', $body); $body = preg_replace('/^ /m', '', $body);
$labelNames = ['standards-update', 'mokocli', 'type: chore', 'automation']; $labelNames = ['standards-update', 'moko-platform', 'type: chore', 'automation'];
$labels = $this->resolveLabelIds($org, $repo, $labelNames); $labels = $this->resolveLabelIds($org, $repo, $labelNames);
try { try {
@@ -1213,7 +1213,7 @@ class BulkSync extends CliFramework
} }
/** /**
* Create a tracking issue in mokocli for this sync run. * Create a tracking issue in moko-platform for this sync run.
*/ */
private function createSyncIssue(string $org, array $results): void private function createSyncIssue(string $org, array $results): void
{ {
@@ -1232,7 +1232,7 @@ class BulkSync extends CliFramework
$issues = $results['issues'] ?? []; $issues = $results['issues'] ?? [];
// Stable title — no timestamp so repeated runs update a single issue // Stable title — no timestamp so repeated runs update a single issue
$title = "sync: mokocli v" . self::VERSION_MINOR . " bulk sync report"; $title = "sync: moko-platform v" . self::VERSION_MINOR . " bulk sync report";
$protection = $results['protection'] ?? []; $protection = $results['protection'] ?? [];
$hasProtect = !empty($protection); $hasProtect = !empty($protection);
@@ -1281,7 +1281,7 @@ class BulkSync extends CliFramework
: "|---|---|---|---|"; : "|---|---|---|---|";
$body = <<<MD $body = <<<MD
## mokocli Bulk Sync Report ## moko-platform Bulk Sync Report
**Organisation:** `{$org}` **Organisation:** `{$org}`
**Triggered:** {$now}{$force} **Triggered:** {$now}{$force}
@@ -1301,7 +1301,7 @@ class BulkSync extends CliFramework
try { try {
// Search for existing issue by label — any state so we can reopen closed ones // Search for existing issue by label — any state so we can reopen closed ones
$existing = $this->api->get("/repos/{$org}/mokocli/issues", [ $existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
'labels' => 'sync-report', 'labels' => 'sync-report',
'state' => 'all', 'state' => 'all',
'per_page' => 1, 'per_page' => 1,
@@ -1309,8 +1309,8 @@ class BulkSync extends CliFramework
'direction' => 'desc', 'direction' => 'desc',
]); ]);
$labelNames = ['sync-report', 'mokocli', 'type: chore', 'automation']; $labelNames = ['sync-report', 'moko-platform', 'type: chore', 'automation'];
$labels = $this->resolveLabelIds($org, 'mokocli', $labelNames); $labels = $this->resolveLabelIds($org, 'moko-platform', $labelNames);
$existing = array_values($existing); $existing = array_values($existing);
if (!empty($existing) && isset($existing[0]['number'])) { if (!empty($existing) && isset($existing[0]['number'])) {
@@ -1319,22 +1319,22 @@ class BulkSync extends CliFramework
if (($existing[0]['state'] ?? 'open') === 'closed') { if (($existing[0]['state'] ?? 'open') === 'closed') {
$patch['state'] = 'open'; $patch['state'] = 'open';
} }
$this->api->patch("/repos/{$org}/mokocli/issues/{$issueNumber}", $patch); $this->api->patch("/repos/{$org}/moko-platform/issues/{$issueNumber}", $patch);
try { try {
$this->api->post("/repos/{$org}/mokocli/issues/{$issueNumber}/labels", ['labels' => $labels]); $this->api->post("/repos/{$org}/moko-platform/issues/{$issueNumber}/labels", ['labels' => $labels]);
} catch (\Exception $le) { } catch (\Exception $le) {
/* non-fatal */ /* non-fatal */
} }
$this->log("📋 Sync report issue updated: {$org}/mokocli#{$issueNumber}", 'INFO'); $this->log("📋 Sync report issue updated: {$org}/moko-platform#{$issueNumber}", 'INFO');
} else { } else {
$issue = $this->api->post("/repos/{$org}/mokocli/issues", [ $issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
'title' => $title, 'title' => $title,
'body' => $body, 'body' => $body,
'labels' => $labels, 'labels' => $labels,
'assignees' => ['jmiller'], 'assignees' => ['jmiller'],
]); ]);
$issueNumber = $issue['number'] ?? '?'; $issueNumber = $issue['number'] ?? '?';
$this->log("📋 Sync report issue created: {$org}/mokocli#{$issueNumber}", 'INFO'); $this->log("📋 Sync report issue created: {$org}/moko-platform#{$issueNumber}", 'INFO');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log("⚠️ Failed to create/update sync report issue: " . $e->getMessage(), 'WARN'); $this->log("⚠️ Failed to create/update sync report issue: " . $e->getMessage(), 'WARN');
@@ -1342,7 +1342,7 @@ class BulkSync extends CliFramework
} }
/** /**
* Create or update a failure issue in mokocli when repos fail to sync. * Create or update a failure issue in moko-platform when repos fail to sync.
* Uses the 'sync-failure' label so it is distinct from the run-report issue. * Uses the 'sync-failure' label so it is distinct from the run-report issue.
* Reopens a closed issue rather than creating a duplicate. * Reopens a closed issue rather than creating a duplicate.
*/ */
@@ -1388,7 +1388,7 @@ class BulkSync extends CliFramework
$body = preg_replace('/^ /m', '', $body); $body = preg_replace('/^ /m', '', $body);
try { try {
$existing = $this->api->get("/repos/{$org}/mokocli/issues", [ $existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
'labels' => 'sync-failure', 'labels' => 'sync-failure',
'state' => 'all', 'state' => 'all',
'per_page' => 1, 'per_page' => 1,
@@ -1403,17 +1403,17 @@ class BulkSync extends CliFramework
if (($existing[0]['state'] ?? 'open') === 'closed') { if (($existing[0]['state'] ?? 'open') === 'closed') {
$patch['state'] = 'open'; $patch['state'] = 'open';
} }
$this->api->patch("/repos/{$org}/mokocli/issues/{$num}", $patch); $this->api->patch("/repos/{$org}/moko-platform/issues/{$num}", $patch);
$this->log("🚨 Failure issue #{$num} updated: {$org}/mokocli#{$num}", 'WARN'); $this->log("🚨 Failure issue #{$num} updated: {$org}/moko-platform#{$num}", 'WARN');
} else { } else {
$issue = $this->api->post("/repos/{$org}/mokocli/issues", [ $issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
'title' => $title, 'title' => $title,
'body' => $body, 'body' => $body,
'labels' => $this->resolveLabelIds($org, 'mokocli', ['sync-failure']), 'labels' => $this->resolveLabelIds($org, 'moko-platform', ['sync-failure']),
'assignees' => ['jmiller'], 'assignees' => ['jmiller'],
]); ]);
$num = $issue['number'] ?? '?'; $num = $issue['number'] ?? '?';
$this->log("🚨 Failure issue created: {$org}/mokocli#{$num}", 'WARN'); $this->log("🚨 Failure issue created: {$org}/moko-platform#{$num}", 'WARN');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN'); $this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
+123 -123
View File
@@ -1,123 +1,123 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Trigger a workflow across all client-waas repos in a Gitea org # BRIEF: Trigger a workflow across all client-waas repos in a Gitea org
set -euo pipefail set -euo pipefail
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Usage # Usage
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") GITEA_URL TOKEN ORG WORKFLOW [REF] [INPUTS] Usage: $(basename "$0") GITEA_URL TOKEN ORG WORKFLOW [REF] [INPUTS]
Arguments: Arguments:
GITEA_URL Base URL of the Gitea instance (e.g. https://git.mokoconsulting.tech) GITEA_URL Base URL of the Gitea instance (e.g. https://git.mokoconsulting.tech)
TOKEN Gitea API token with repo/action permissions TOKEN Gitea API token with repo/action permissions
ORG Organisation or user that owns the repos ORG Organisation or user that owns the repos
WORKFLOW Workflow filename to trigger (e.g. dependency-audit.yml) WORKFLOW Workflow filename to trigger (e.g. dependency-audit.yml)
REF Branch ref to run against (default: main) REF Branch ref to run against (default: main)
INPUTS Optional JSON object of workflow inputs (e.g. '{"dry_run":"true"}') INPUTS Optional JSON object of workflow inputs (e.g. '{"dry_run":"true"}')
Example: Example:
$(basename "$0") https://git.mokoconsulting.tech abc123 MokoConsulting dependency-audit.yml main '{"notify":"true"}' $(basename "$0") https://git.mokoconsulting.tech abc123 MokoConsulting dependency-audit.yml main '{"notify":"true"}'
EOF EOF
exit 1 exit 1
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Argument parsing # Argument parsing
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [ $# -lt 4 ]; then if [ $# -lt 4 ]; then
usage usage
fi fi
GITEA_URL="${1%/}" GITEA_URL="${1%/}"
TOKEN="$2" TOKEN="$2"
ORG="$3" ORG="$3"
WORKFLOW="$4" WORKFLOW="$4"
REF="${5:-main}" REF="${5:-main}"
INPUTS="${6:-{\}}" INPUTS="${6:-{\}}"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Fetch all repos in the org, paginated # Fetch all repos in the org, paginated
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
echo "Fetching repos for org '${ORG}' on ${GITEA_URL} ..." echo "Fetching repos for org '${ORG}' on ${GITEA_URL} ..."
PAGE=1 PAGE=1
LIMIT=50 LIMIT=50
ALL_REPOS="" ALL_REPOS=""
while true; do while true; do
RESPONSE=$(curl -s \ RESPONSE=$(curl -s \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
-H "Accept: application/json" \ -H "Accept: application/json" \
"${GITEA_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=${LIMIT}") "${GITEA_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=${LIMIT}")
# Break if empty array # Break if empty array
COUNT=$(echo "$RESPONSE" | jq -r 'length') COUNT=$(echo "$RESPONSE" | jq -r 'length')
if [ "$COUNT" -eq 0 ]; then if [ "$COUNT" -eq 0 ]; then
break break
fi fi
NAMES=$(echo "$RESPONSE" | jq -r '.[].name') NAMES=$(echo "$RESPONSE" | jq -r '.[].name')
ALL_REPOS="${ALL_REPOS}${NAMES}"$'\n' ALL_REPOS="${ALL_REPOS}${NAMES}"$'\n'
if [ "$COUNT" -lt "$LIMIT" ]; then if [ "$COUNT" -lt "$LIMIT" ]; then
break break
fi fi
PAGE=$((PAGE + 1)) PAGE=$((PAGE + 1))
done done
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Filter for client-waas repos # Filter for client-waas repos
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CLIENT_REPOS=$(echo "$ALL_REPOS" | grep 'client-waas' | sort || true) CLIENT_REPOS=$(echo "$ALL_REPOS" | grep 'client-waas' | sort || true)
if [ -z "$CLIENT_REPOS" ]; then if [ -z "$CLIENT_REPOS" ]; then
echo "No client-waas repos found in org '${ORG}'." echo "No client-waas repos found in org '${ORG}'."
exit 0 exit 0
fi fi
TOTAL=$(echo "$CLIENT_REPOS" | wc -l | tr -d ' ') TOTAL=$(echo "$CLIENT_REPOS" | wc -l | tr -d ' ')
echo "Found ${TOTAL} client-waas repo(s). Triggering workflow '${WORKFLOW}' (ref: ${REF}) ..." echo "Found ${TOTAL} client-waas repo(s). Triggering workflow '${WORKFLOW}' (ref: ${REF}) ..."
echo "" echo ""
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Trigger workflow for each repo # Trigger workflow for each repo
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
SUCCESS=0 SUCCESS=0
FAIL=0 FAIL=0
while IFS= read -r REPO; do while IFS= read -r REPO; do
[ -z "$REPO" ] && continue [ -z "$REPO" ] && continue
PAYLOAD=$(jq -n --arg ref "$REF" --argjson inputs "$INPUTS" '{ref: $ref, inputs: $inputs}') PAYLOAD=$(jq -n --arg ref "$REF" --argjson inputs "$INPUTS" '{ref: $ref, inputs: $inputs}')
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \ HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
-X POST \ -X POST \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$PAYLOAD" \ -d "$PAYLOAD" \
"${GITEA_URL}/api/v1/repos/${ORG}/${REPO}/actions/workflows/${WORKFLOW}/dispatches") "${GITEA_URL}/api/v1/repos/${ORG}/${REPO}/actions/workflows/${WORKFLOW}/dispatches")
if [ "$HTTP_CODE" -eq 204 ] || [ "$HTTP_CODE" -eq 201 ]; then if [ "$HTTP_CODE" -eq 204 ] || [ "$HTTP_CODE" -eq 201 ]; then
echo " [OK] ${ORG}/${REPO} (HTTP ${HTTP_CODE})" echo " [OK] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
SUCCESS=$((SUCCESS + 1)) SUCCESS=$((SUCCESS + 1))
else else
echo " [FAIL] ${ORG}/${REPO} (HTTP ${HTTP_CODE})" echo " [FAIL] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
FAIL=$((FAIL + 1)) FAIL=$((FAIL + 1))
fi fi
done <<< "$CLIENT_REPOS" done <<< "$CLIENT_REPOS"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Summary # Summary
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
echo "" echo ""
echo "Done. Success: ${SUCCESS} | Failed: ${FAIL} | Total: ${TOTAL}" echo "Done. Success: ${SUCCESS} | Failed: ${FAIL} | Total: ${TOTAL}"
if [ "$FAIL" -gt 0 ]; then if [ "$FAIL" -gt 0 ]; then
exit 1 exit 1
fi fi
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
+9 -9
View File
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform * INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/enrich_manifest_xml.php * PATH: /automation/enrich_manifest_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details * BRIEF: Enrich XML manifests with repo-specific build and deploy details
* *
@@ -21,8 +21,8 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\ManifestParser; use MokoEnterprise\MokoStandardsParser;
class EnrichManifestXmlCli extends CliFramework class EnrichManifestXmlCli extends CliFramework
{ {
@@ -43,10 +43,10 @@ class EnrichManifestXmlCli extends CliFramework
$skipStr = $this->getArgument('--skip'); $skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : []; $skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser(); $parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid(); $tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
echo "=== mokocli XML Manifest Enrichment ===\n"; echo "=== moko-platform XML Manifest Enrichment ===\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n"; echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) { if (!empty($skipRepos)) {
echo "Skipping: " . implode(', ', $skipRepos) . "\n"; echo "Skipping: " . implode(', ', $skipRepos) . "\n";
@@ -97,7 +97,7 @@ class EnrichManifestXmlCli extends CliFramework
} }
$manifestPath = "{$workDir}/.mokogitea/manifest.xml"; $manifestPath = "{$workDir}/.mokogitea/manifest.xml";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokocli')) { if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<moko-platform')) {
echo "SKIP (no XML manifest)\n"; echo "SKIP (no XML manifest)\n";
$stats['skipped']++; $stats['skipped']++;
$this->rmTree($workDir); $this->rmTree($workDir);
@@ -113,8 +113,8 @@ class EnrichManifestXmlCli extends CliFramework
} }
$enrichment['build']['language'] = $enrichment['build']['language'] $enrichment['build']['language'] = $enrichment['build']['language']
?? $repo['language'] ?? $repo['language']
?? ManifestParser::platformLanguage($platform); ?? MokoStandardsParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform); $enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment); $enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []); $dc = count($enrichment['deploy'] ?? []);
@@ -312,7 +312,7 @@ class EnrichManifestXmlCli extends CliFramework
return $xml; return $xml;
} }
$ns = ManifestParser::NAMESPACE_URI; $ns = MokoStandardsParser::NAMESPACE_URI;
$root = $dom->documentElement; $root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) { foreach (['build', 'deploy', 'scripts'] as $tag) {
+9 -9
View File
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform * INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/enrich_mokostandards_xml.php * PATH: /automation/enrich_mokostandards_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details * BRIEF: Enrich XML manifests with repo-specific build and deploy details
* *
@@ -21,8 +21,8 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\ManifestParser; use MokoEnterprise\MokoStandardsParser;
class EnrichMokostandardsXmlCli extends CliFramework class EnrichMokostandardsXmlCli extends CliFramework
{ {
@@ -43,10 +43,10 @@ class EnrichMokostandardsXmlCli extends CliFramework
$skipStr = $this->getArgument('--skip'); $skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : []; $skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser(); $parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid(); $tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
echo "=== mokocli XML Manifest Enrichment ===\n"; echo "=== moko-platform XML Manifest Enrichment ===\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n"; echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) { if (!empty($skipRepos)) {
echo "Skipping: " . implode(', ', $skipRepos) . "\n"; echo "Skipping: " . implode(', ', $skipRepos) . "\n";
@@ -97,7 +97,7 @@ class EnrichMokostandardsXmlCli extends CliFramework
} }
$manifestPath = "{$workDir}/.mokogitea/manifest.xml"; $manifestPath = "{$workDir}/.mokogitea/manifest.xml";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokocli')) { if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<moko-platform')) {
echo "SKIP (no XML manifest)\n"; echo "SKIP (no XML manifest)\n";
$stats['skipped']++; $stats['skipped']++;
$this->rmTree($workDir); $this->rmTree($workDir);
@@ -113,8 +113,8 @@ class EnrichMokostandardsXmlCli extends CliFramework
} }
$enrichment['build']['language'] = $enrichment['build']['language'] $enrichment['build']['language'] = $enrichment['build']['language']
?? $repo['language'] ?? $repo['language']
?? ManifestParser::platformLanguage($platform); ?? MokoStandardsParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform); $enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment); $enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []); $dc = count($enrichment['deploy'] ?? []);
@@ -315,7 +315,7 @@ class EnrichMokostandardsXmlCli extends CliFramework
return $xml; return $xml;
} }
$ns = ManifestParser::NAMESPACE_URI; $ns = MokoStandardsParser::NAMESPACE_URI;
$root = $dom->documentElement; $root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) { foreach (['build', 'deploy', 'scripts'] as $tag) {
+1 -1
View File
@@ -4,7 +4,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION FILE INFORMATION
DEFGROUP: MokoPlatform.Index DEFGROUP: MokoPlatform.Index
INGROUP: MokoPlatform.Automation INGROUP: MokoPlatform.Automation
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /automation/index.md PATH: /automation/index.md
BRIEF: Automation directory index BRIEF: Automation directory index
--> -->
+9 -9
View File
@@ -10,14 +10,14 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform * INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/migrate_to_gitea.php * PATH: /automation/migrate_to_gitea.php
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance * BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
* *
* USAGE * USAGE
* php automation/migrate_to_gitea.php --dry-run * php automation/migrate_to_gitea.php --dry-run
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods * php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
* php automation/migrate_to_gitea.php --exclude mokocli --skip-archived * php automation/migrate_to_gitea.php --exclude moko-platform --skip-archived
* php automation/migrate_to_gitea.php --resume * php automation/migrate_to_gitea.php --resume
*/ */
@@ -25,12 +25,12 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
use MokoCli\CheckpointManager; use MokoEnterprise\CheckpointManager;
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\Config; use MokoEnterprise\Config;
use MokoCli\PlatformAdapterFactory; use MokoEnterprise\PlatformAdapterFactory;
use MokoCli\GitHubAdapter; use MokoEnterprise\GitHubAdapter;
use MokoCli\MokoGiteaAdapter; use MokoEnterprise\MokoGiteaAdapter;
/** /**
* Gitea Migration Script * Gitea Migration Script
@@ -278,7 +278,7 @@ class MigrateToGitea extends CliFramework
try { try {
$this->gitea->createIssue( $this->gitea->createIssue(
$giteaOrg, $giteaOrg,
'mokocli', 'moko-platform',
'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated', 'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated',
$report, $report,
['labels' => ['automation', 'type: chore']] ['labels' => ['automation', 'type: chore']]
+22 -23
View File
@@ -11,7 +11,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform.Scripts * INGROUP: MokoPlatform.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_files.php * PATH: /automation/push_files.php
* BRIEF: Push one or more specific files to one or more remote repositories * BRIEF: Push one or more specific files to one or more remote repositories
*/ */
@@ -21,7 +21,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{ use MokoEnterprise\{
ApiClient, ApiClient,
AuditLogger, AuditLogger,
CliFramework, CliFramework,
@@ -35,7 +35,7 @@ use MokoCli\{
/** /**
* Targeted File Push Tool * Targeted File Push Tool
* *
* Pushes one or more specific files from mokocli templates to one or * Pushes one or more specific files from moko-platform templates to one or
* more remote repositories — without running a full sync. * more remote repositories — without running a full sync.
* *
* Files are specified by their destination path as they appear in the target * Files are specified by their destination path as they appear in the target
@@ -81,7 +81,7 @@ class PushFiles extends CliFramework
*/ */
protected function run(): int protected function run(): int
{ {
$this->log('📦 mokocli File Push v' . self::VERSION, 'INFO'); $this->log('📦 moko-platform File Push v' . self::VERSION, 'INFO');
if (!$this->initializeComponents()) { if (!$this->initializeComponents()) {
return 1; return 1;
@@ -230,8 +230,7 @@ class PushFiles extends CliFramework
{ {
// Read platform from repo's .mokogitea/manifest.xml via API // Read platform from repo's .mokogitea/manifest.xml via API
try { try {
$fileInfo = $this->adapter->getFileContents($org, $repo, '.mokogitea/manifest.xml', 'main'); $manifestData = $this->adapter->getFileContent($org, $repo, '.mokogitea/manifest.xml', 'main');
$manifestData = isset($fileInfo['content']) ? base64_decode($fileInfo['content']) : '';
if (!empty($manifestData)) { if (!empty($manifestData)) {
$xml = @simplexml_load_string($manifestData); $xml = @simplexml_load_string($manifestData);
if ($xml !== false) { if ($xml !== false) {
@@ -337,7 +336,7 @@ class PushFiles extends CliFramework
$prNumber = null; $prNumber = null;
if (!$direct) { if (!$direct) {
$prTitle = "chore: push " . count($entries) . " file(s) from mokocli"; $prTitle = "chore: push " . count($entries) . " file(s) from moko-platform";
$prBody = $this->buildPRBody($entries); $prBody = $this->buildPRBody($entries);
$pr = $this->adapter->createPullRequest( $pr = $this->adapter->createPullRequest(
$org, $org,
@@ -414,7 +413,7 @@ class PushFiles extends CliFramework
$message = !empty($customMessage) $message = !empty($customMessage)
? $customMessage ? $customMessage
: "chore: update {$destPath} from mokocli"; : "chore: update {$destPath} from moko-platform";
// Fetch existing file SHA (needed for updates) // Fetch existing file SHA (needed for updates)
$existingSha = null; $existingSha = null;
@@ -457,9 +456,9 @@ class PushFiles extends CliFramework
): void { ): void {
$now = gmdate('Y-m-d H:i:s') . ' UTC'; $now = gmdate('Y-m-d H:i:s') . ' UTC';
$version = self::VERSION; $version = self::VERSION;
$source = $this->adapter->getRepoWebUrl($org, 'mokocli'); $source = $this->adapter->getRepoWebUrl($org, 'moko-platform');
$title = "chore: mokocli file push tracking"; $title = "chore: moko-platform file push tracking";
$deliveryLine = $prNumber !== null $deliveryLine = $prNumber !== null
? "| **Pull request** | [#{$prNumber}](" . $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber) . ") |" ? "| **Pull request** | [#{$prNumber}](" . $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber) . ") |"
@@ -471,9 +470,9 @@ class PushFiles extends CliFramework
)); ));
$body = <<<MD $body = <<<MD
## mokocli File Push ## moko-platform File Push
One or more files were pushed to this repository from mokocli. One or more files were pushed to this repository from moko-platform.
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
@@ -487,12 +486,12 @@ class PushFiles extends CliFramework
{$fileRows} {$fileRows}
--- ---
*Generated automatically by [mokocli]({$source}) `push_files.php`* *Generated automatically by [moko-platform]({$source}) `push_files.php`*
MD; MD;
$body = preg_replace('/^ /m', '', $body); $body = preg_replace('/^ /m', '', $body);
$labels = ['standards-update', 'mokocli', 'type: chore', 'automation']; $labels = ['standards-update', 'moko-platform', 'type: chore', 'automation'];
try { try {
$existing = $this->api->get("/repos/{$org}/{$repo}/issues", [ $existing = $this->api->get("/repos/{$org}/{$repo}/issues", [
@@ -550,7 +549,7 @@ class PushFiles extends CliFramework
} }
/** /**
* Create or update a failure issue in mokocli when repos fail to receive files. * Create or update a failure issue in moko-platform when repos fail to receive files.
* Uses the 'push-failure' label. Reopens a closed issue rather than creating a duplicate. * Uses the 'push-failure' label. Reopens a closed issue rather than creating a duplicate.
*/ */
private function createFailureIssue(string $org, array $results): void private function createFailureIssue(string $org, array $results): void
@@ -598,7 +597,7 @@ class PushFiles extends CliFramework
$body = preg_replace('/^ /m', '', $body); $body = preg_replace('/^ /m', '', $body);
try { try {
$existing = $this->api->get("/repos/{$org}/mokocli/issues", [ $existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
'labels' => 'push-failure', 'labels' => 'push-failure',
'state' => 'all', 'state' => 'all',
'per_page' => 1, 'per_page' => 1,
@@ -613,17 +612,17 @@ class PushFiles extends CliFramework
if (($existing[0]['state'] ?? 'open') === 'closed') { if (($existing[0]['state'] ?? 'open') === 'closed') {
$patch['state'] = 'open'; $patch['state'] = 'open';
} }
$this->api->patch("/repos/{$org}/mokocli/issues/{$num}", $patch); $this->api->patch("/repos/{$org}/moko-platform/issues/{$num}", $patch);
$this->log("🚨 Failure issue #{$num} updated: {$org}/mokocli#{$num}", 'WARN'); $this->log("🚨 Failure issue #{$num} updated: {$org}/moko-platform#{$num}", 'WARN');
} else { } else {
$issue = $this->api->post("/repos/{$org}/mokocli/issues", [ $issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
'title' => $title, 'title' => $title,
'body' => $body, 'body' => $body,
'labels' => ['push-failure'], 'labels' => ['push-failure'],
'assignees' => ['jmiller'], 'assignees' => ['jmiller'],
]); ]);
$num = $issue['number'] ?? '?'; $num = $issue['number'] ?? '?';
$this->log("🚨 Failure issue created: {$org}/mokocli#{$num}", 'WARN'); $this->log("🚨 Failure issue created: {$org}/moko-platform#{$num}", 'WARN');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN'); $this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
@@ -638,14 +637,14 @@ class PushFiles extends CliFramework
private function buildPRBody(array $entries): string private function buildPRBody(array $entries): string
{ {
$now = gmdate('Y-m-d H:i:s') . ' UTC'; $now = gmdate('Y-m-d H:i:s') . ' UTC';
$lines = ["## mokocli File Push\n", "**Pushed:** {$now}\n", '### Files\n']; $lines = ["## moko-platform File Push\n", "**Pushed:** {$now}\n", '### Files\n'];
foreach ($entries as $entry) { foreach ($entries as $entry) {
$lines[] = "- `{$entry['destination']}`"; $lines[] = "- `{$entry['destination']}`";
} }
$sourceUrl = $this->adapter->getRepoWebUrl(self::DEFAULT_ORG, 'mokocli'); $sourceUrl = $this->adapter->getRepoWebUrl(self::DEFAULT_ORG, 'moko-platform');
$lines[] = "\n---\n*Generated by [mokocli]({$sourceUrl}) `push_files.php`*"; $lines[] = "\n---\n*Generated by [moko-platform]({$sourceUrl}) `push_files.php`*";
return implode("\n", $lines); return implode("\n", $lines);
} }
+8 -8
View File
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform * INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_manifest_xml.php * PATH: /automation/push_manifest_xml.php
* BRIEF: Push XML manifests to all governed repositories * BRIEF: Push XML manifests to all governed repositories
*/ */
@@ -18,8 +18,8 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\ManifestParser; use MokoEnterprise\MokoStandardsParser;
class PushManifestXmlCli extends CliFramework class PushManifestXmlCli extends CliFramework
{ {
@@ -44,10 +44,10 @@ class PushManifestXmlCli extends CliFramework
$skipStr = $this->getArgument('--skip'); $skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : []; $skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser(); $parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid(); $tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
echo "=== mokocli XML Manifest Push ===\n"; echo "=== moko-platform XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n"; echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n"; echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) { if ($repoFilter) {
@@ -97,8 +97,8 @@ class PushManifestXmlCli extends CliFramework
'description' => $repo['description'] ?? '', 'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later', 'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [], 'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform), 'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
'package_type' => ManifestParser::platformPackageType($platform), 'package_type' => MokoStandardsParser::platformPackageType($platform),
'last_synced' => date('c'), 'last_synced' => date('c'),
]); ]);
@@ -125,7 +125,7 @@ class PushManifestXmlCli extends CliFramework
// Check if already XML and up-to-date // Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/manifest.xml"; $manifestPath = "{$workDir}/.mokogitea/manifest.xml";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokocli'); $existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<moko-platform');
if ($existingIsXml && !$force) { if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath)); $existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) { if ($existingPlatform === $platform) {
+8 -8
View File
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform * INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_mokostandards_xml.php * PATH: /automation/push_mokostandards_xml.php
* BRIEF: Push XML manifests to all governed repositories * BRIEF: Push XML manifests to all governed repositories
*/ */
@@ -18,8 +18,8 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\ManifestParser; use MokoEnterprise\MokoStandardsParser;
class PushMokostandardsXmlCli extends CliFramework class PushMokostandardsXmlCli extends CliFramework
{ {
@@ -44,10 +44,10 @@ class PushMokostandardsXmlCli extends CliFramework
$skipStr = $this->getArgument('--skip'); $skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : []; $skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new ManifestParser(); $parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid(); $tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
echo "=== mokocli XML Manifest Push ===\n"; echo "=== moko-platform XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n"; echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n"; echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) { if ($repoFilter) {
@@ -97,8 +97,8 @@ class PushMokostandardsXmlCli extends CliFramework
'description' => $repo['description'] ?? '', 'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later', 'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [], 'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform), 'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
'package_type' => ManifestParser::platformPackageType($platform), 'package_type' => MokoStandardsParser::platformPackageType($platform),
'last_synced' => date('c'), 'last_synced' => date('c'),
]); ]);
@@ -125,7 +125,7 @@ class PushMokostandardsXmlCli extends CliFramework
// Check if already XML and up-to-date // Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/manifest.xml"; $manifestPath = "{$workDir}/.mokogitea/manifest.xml";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokocli'); $existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<moko-platform');
if ($existingIsXml && !$force) { if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath)); $existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) { if ($existingPlatform === $platform) {
+10 -10
View File
@@ -11,7 +11,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation * DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform.Scripts * INGROUP: MokoPlatform.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/repo_cleanup.php * PATH: /automation/repo_cleanup.php
* BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs * BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs
*/ */
@@ -21,7 +21,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory}; use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
/** /**
* Enterprise Repository Cleanup * Enterprise Repository Cleanup
@@ -39,14 +39,14 @@ use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, M
class RepoCleanup extends CliFramework class RepoCleanup extends CliFramework
{ {
private const VERSION = '09.23.00'; private const VERSION = '09.23.00';
private const SYNC_PREFIX = 'chore/sync-mokocli-'; private const SYNC_PREFIX = 'chore/sync-moko-platform-';
private const CURRENT_BRANCH = 'chore/sync-mokocli-v04.02.00'; private const CURRENT_BRANCH = 'chore/sync-moko-platform-v04.02.00';
/** Workflow files that have been retired and should be deleted from governed repos. */ /** Workflow files that have been retired and should be deleted from governed repos. */
private const RETIRED_WORKFLOWS = [ private const RETIRED_WORKFLOWS = [
'build.yml', 'code-quality.yml', 'release-cycle.yml', 'release-pipeline.yml', 'build.yml', 'code-quality.yml', 'release-cycle.yml', 'release-pipeline.yml',
'branch-cleanup.yml', 'auto-update-changelog.yml', 'enterprise-issue-manager.yml', 'branch-cleanup.yml', 'auto-update-changelog.yml', 'enterprise-issue-manager.yml',
'flush-actions-cache.yml', 'mokocli-script-runner.yml', 'unified-ci.yml', 'flush-actions-cache.yml', 'moko-platform-script-runner.yml', 'unified-ci.yml',
'unified-platform-testing.yml', 'reusable-build.yml', 'reusable-ci-validation.yml', 'unified-platform-testing.yml', 'reusable-build.yml', 'reusable-ci-validation.yml',
'reusable-deploy.yml', 'reusable-php-quality.yml', 'reusable-platform-testing.yml', 'reusable-deploy.yml', 'reusable-php-quality.yml', 'reusable-platform-testing.yml',
'reusable-project-detector.yml', 'reusable-release.yml', 'reusable-script-executor.yml', 'reusable-project-detector.yml', 'reusable-release.yml', 'reusable-script-executor.yml',
@@ -98,7 +98,7 @@ class RepoCleanup extends CliFramework
} }
$this->logMsg("🧹 mokocli Repository Cleanup v" . self::VERSION); $this->logMsg("🧹 moko-platform Repository Cleanup v" . self::VERSION);
$this->logMsg("Organization: {$org}"); $this->logMsg("Organization: {$org}");
$this->logMsg("Current sync branch: " . self::CURRENT_BRANCH); $this->logMsg("Current sync branch: " . self::CURRENT_BRANCH);
if ($this->dryRun) { if ($this->dryRun) {
@@ -225,7 +225,7 @@ class RepoCleanup extends CliFramework
} }
$allRepos = $this->adapter->listOrgRepos($org, $skipArchived); $allRepos = $this->adapter->listOrgRepos($org, $skipArchived);
return array_filter($allRepos, fn($r) => !in_array($r['name'], ['mokocli', '.github-private'], true)); return array_filter($allRepos, fn($r) => !in_array($r['name'], ['moko-platform', '.github-private'], true));
} }
// ─── Cleanup operations ────────────────────────────────────────────── // ─── Cleanup operations ──────────────────────────────────────────────
@@ -463,9 +463,9 @@ class RepoCleanup extends CliFramework
private function checkLabels(string $org, string $repo, array &$results): void private function checkLabels(string $org, string $repo, array &$results): void
{ {
try { try {
$this->api->get("/repos/{$org}/{$repo}/labels/mokocli"); $this->api->get("/repos/{$org}/{$repo}/labels/moko-platform");
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logMsg(" ⚠️ Missing 'mokocli' label"); $this->logMsg(" ⚠️ Missing 'moko-platform' label");
$results['labels_missing']++; $results['labels_missing']++;
$this->api->resetCircuitBreaker(); $this->api->resetCircuitBreaker();
} }
@@ -479,7 +479,7 @@ class RepoCleanup extends CliFramework
if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) {
$version = $m[1]; $version = $m[1];
// Check manifest.xml for the tracked mokocli version // Check manifest.xml for the tracked moko-platform version
try { try {
$mokoFile = $this->api->get("/repos/{$org}/{$repo}/contents/.mokogitea/manifest.xml"); $mokoFile = $this->api->get("/repos/{$org}/{$repo}/contents/.mokogitea/manifest.xml");
$mokoContent = base64_decode($mokoFile['content'] ?? ''); $mokoContent = base64_decode($mokoFile['content'] ?? '');
+1 -1
View File
@@ -6,7 +6,7 @@
# #
# DEFGROUP: MokoPlatform.Automation.ServerAutoheal # DEFGROUP: MokoPlatform.Automation.ServerAutoheal
# INGROUP: MokoPlatform.Automation # INGROUP: MokoPlatform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/server-autoheal.sh # PATH: /automation/server-autoheal.sh
# BRIEF: Server auto-heal on unclean restart + split system/content backups # BRIEF: Server auto-heal on unclean restart + split system/content backups
# #
-633
View File
@@ -1,633 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/update_dependencies.php
* VERSION: 09.38.00
* BRIEF: Cross-repo dependency update automation — scan, update, PR, auto-merge
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoCli\{
ApiClient,
AuditLogger,
CheckpointManager,
CircuitBreakerOpen,
CliFramework,
Config,
GitPlatformAdapter,
PlatformAdapterFactory,
RateLimitExceeded
};
/**
* Cross-Repo Dependency Update Automation
*
* Scans org repos for outdated Composer/npm dependencies, creates PRs with
* changelogs, and optionally auto-merges safe patch updates.
*
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/149
*/
class UpdateDependencies extends CliFramework
{
public const VERSION = '01.00.00';
private const BRANCH_PREFIX = 'chore/deps-update';
private ApiClient $api;
private GitPlatformAdapter $adapter;
private AuditLogger $logger;
private CheckpointManager $checkpoints;
/** Summary counters. */
private int $reposScanned = 0;
private int $reposUpdated = 0;
private int $prsCreated = 0;
private int $autoMerged = 0;
private int $reposFailed = 0;
protected function configure(): void
{
$this->setDescription('Cross-repo dependency update automation');
$this->addArgument('--org', 'Organization to scan', 'MokoConsulting');
$this->addArgument('--repos', 'Comma-separated list of specific repos', '');
$this->addArgument('--exclude', 'Comma-separated list of repos to exclude', '');
$this->addArgument('--skip-archived', 'Skip archived repositories', true);
$this->addArgument('--type', 'Dependency type: composer, npm, or all', 'all');
$this->addArgument('--patch-only', 'Only update patch versions (safe updates)', false);
$this->addArgument('--auto-merge', 'Auto-merge PRs with only patch updates', false);
$this->addArgument('--resume', 'Resume from checkpoint', false);
}
protected function run(): int
{
$this->log("Dependency Update Automation v" . self::VERSION, 'INFO');
if (!$this->initComponents()) {
return self::EXIT_FAILURE;
}
$org = $this->getArgument('--org', 'MokoConsulting');
$depType = strtolower($this->getArgument('--type', 'all'));
$patchOnly = $this->getArgument('--patch-only', false);
$autoMerge = $this->getArgument('--auto-merge', false);
// ── Gather repos ─────────────────────────────────────────────────
$repos = $this->gatherRepos($org);
if ($repos === null) {
return self::EXIT_FAILURE;
}
$total = count($repos);
$this->log("Found {$total} repositories to scan", 'INFO');
// ── Resume support ───────────────────────────────────────────────
$completed = [];
if ($this->getArgument('--resume', false)) {
$checkpoint = $this->checkpoints->load('deps_update');
if ($checkpoint) {
$completed = $checkpoint['completed'] ?? [];
$this->log("Resuming — skipping " . count($completed) . " already-processed repos", 'INFO');
}
}
// ── Process each repo ────────────────────────────────────────────
$this->section('Scanning repositories for outdated dependencies');
foreach ($repos as $i => $repo) {
$repoName = $repo['name'];
$this->progress($i + 1, $total, $repoName);
if (in_array($repoName, $completed, true)) {
continue;
}
try {
$this->processRepo($org, $repoName, $depType, $patchOnly, $autoMerge);
$completed[] = $repoName;
$this->checkpoints->save('deps_update', ['completed' => $completed]);
} catch (RateLimitExceeded $e) {
$this->log("Rate limit hit — checkpoint saved", 'WARNING');
break;
} catch (CircuitBreakerOpen $e) {
$this->log("Circuit breaker open — checkpoint saved", 'WARNING');
break;
} catch (\Exception $e) {
$this->log("Failed {$repoName}: {$e->getMessage()}", 'ERROR');
$this->reposFailed++;
}
}
$this->progress($total, $total, '', true);
// ── Summary ──────────────────────────────────────────────────────
$this->section('Summary');
$this->printSummary(
$this->reposScanned - $this->reposFailed,
$this->reposFailed,
$this->elapsed()
);
$this->log("Repos scanned: {$this->reposScanned}", 'INFO');
$this->log("Repos updated: {$this->reposUpdated}", 'INFO');
$this->log("PRs created: {$this->prsCreated}", 'INFO');
if ($autoMerge) {
$this->log("Auto-merged: {$this->autoMerged}", 'INFO');
}
if (count($completed) === $total) {
$this->checkpoints->clear('deps_update');
}
return $this->reposFailed > 0 ? self::EXIT_FAILURE : self::EXIT_SUCCESS;
}
// ── Component init ───────────────────────────────────────────────────
private function initComponents(): bool
{
try {
$config = new Config();
$this->api = new ApiClient($config);
$this->adapter = PlatformAdapterFactory::create($this->api, $config);
$this->logger = new AuditLogger();
$this->checkpoints = new CheckpointManager();
return true;
} catch (\Exception $e) {
$this->log("Failed to initialise: {$e->getMessage()}", 'ERROR');
return false;
}
}
// ── Repo gathering ───────────────────────────────────────────────────
private function gatherRepos(string $org): ?array
{
$specificRepos = array_filter(explode(',', $this->getArgument('--repos', '')));
$excludeRepos = array_filter(explode(',', $this->getArgument('--exclude', '')));
$skipArchived = $this->getArgument('--skip-archived', true);
// Default exclusions
$excludeRepos = array_merge($excludeRepos, [
'mokocli', '.mokogitea-private', 'org-profile',
]);
try {
$repos = $this->adapter->listOrgRepos($org, $skipArchived);
} catch (\Exception $e) {
$this->log("Failed to list repos: {$e->getMessage()}", 'ERROR');
return null;
}
if (!empty($specificRepos)) {
$repos = array_filter($repos, fn($r) => in_array($r['name'], $specificRepos, true));
}
if (!empty($excludeRepos)) {
$repos = array_filter($repos, fn($r) => !in_array($r['name'], $excludeRepos, true));
}
return array_values($repos);
}
// ── Per-repo processing ──────────────────────────────────────────────
private function processRepo(
string $org,
string $repoName,
string $depType,
bool $patchOnly,
bool $autoMerge
): void {
$this->reposScanned++;
$hasComposer = ($depType === 'all' || $depType === 'composer');
$hasNpm = ($depType === 'all' || $depType === 'npm');
$outdated = [];
// ── Composer ─────────────────────────────────────────────────
if ($hasComposer) {
$composerOutdated = $this->scanComposer($org, $repoName, $patchOnly);
if ($composerOutdated !== null) {
$outdated['composer'] = $composerOutdated;
}
}
// ── npm ──────────────────────────────────────────────────────
if ($hasNpm) {
$npmOutdated = $this->scanNpm($org, $repoName, $patchOnly);
if ($npmOutdated !== null) {
$outdated['npm'] = $npmOutdated;
}
}
if (empty($outdated)) {
return;
}
// Check if there's already an open deps PR
if ($this->hasExistingDepsPR($org, $repoName)) {
$this->log(" {$repoName}: existing deps PR found — skipping", 'INFO');
return;
}
$this->reposUpdated++;
// ── Create PR ────────────────────────────────────────────────
$totalUpdates = 0;
$allPatchOnly = true;
foreach ($outdated as $type => $packages) {
$totalUpdates += count($packages);
foreach ($packages as $pkg) {
if (!$this->isPatchUpdate($pkg['current'] ?? '', $pkg['latest'] ?? '')) {
$allPatchOnly = false;
}
}
}
$title = "chore(deps): update {$totalUpdates} " . ($totalUpdates === 1 ? 'dependency' : 'dependencies');
$body = $this->buildPrBody($repoName, $outdated);
$branch = self::BRANCH_PREFIX . '-' . date('Y-m-d');
if ($this->dryRun) {
$this->log("[dry-run] Would create PR in {$repoName}: {$title}", 'INFO');
foreach ($outdated as $type => $packages) {
foreach ($packages as $pkg) {
$this->log(" [{$type}] {$pkg['name']}: {$pkg['current']}{$pkg['latest']}", 'INFO');
}
}
return;
}
try {
// Clone repo, run updates, push branch
$prNumber = $this->cloneUpdateAndPR($org, $repoName, $branch, $title, $body, $outdated);
if ($prNumber > 0) {
$this->prsCreated++;
$this->log(" {$repoName}: PR #{$prNumber} created", 'INFO');
// Auto-merge if all updates are patch-level
if ($autoMerge && $allPatchOnly && $prNumber > 0) {
$this->tryAutoMerge($org, $repoName, $prNumber);
}
}
} catch (\Exception $e) {
$this->log(" {$repoName}: PR creation failed — {$e->getMessage()}", 'ERROR');
}
}
// ── Composer scanning ────────────────────────────────────────────────
private function scanComposer(string $org, string $repoName, bool $patchOnly): ?array
{
// Check if repo has composer.json
try {
$this->adapter->getFileContents($org, $repoName, 'composer.json');
} catch (\Exception $e) {
return null;
}
// Check if repo has composer.lock
try {
$this->adapter->getFileContents($org, $repoName, 'composer.lock');
} catch (\Exception $e) {
return null;
}
// Clone to temp dir and run composer outdated
$tmpDir = sys_get_temp_dir() . '/moko_deps_' . $repoName . '_' . getmypid();
@mkdir($tmpDir, 0700, true);
try {
$cloneUrl = $this->adapter->getCloneUrl($org, $repoName);
$cmd = sprintf(
'git clone --depth 1 --quiet %s %s 2>/dev/null',
escapeshellarg($cloneUrl),
escapeshellarg($tmpDir)
);
exec($cmd, $output, $exitCode);
if ($exitCode !== 0) {
return null;
}
// Run composer outdated
$flags = $patchOnly ? '--minor-only' : '';
$cmd = sprintf(
'composer outdated --format=json --no-interaction %s --working-dir=%s 2>/dev/null',
$flags,
escapeshellarg($tmpDir)
);
$json = shell_exec($cmd);
if ($json === null || $json === '') {
return null;
}
$data = json_decode($json, true);
$installed = $data['installed'] ?? [];
if (empty($installed)) {
return null;
}
$outdated = [];
foreach ($installed as $pkg) {
// Skip abandoned/dev packages
if (($pkg['abandoned'] ?? false) || str_starts_with($pkg['version'] ?? '', 'dev-')) {
continue;
}
$outdated[] = [
'name' => $pkg['name'] ?? '',
'current' => $pkg['version'] ?? '',
'latest' => $pkg['latest'] ?? '',
'status' => $pkg['latest-status'] ?? 'unknown',
];
}
return empty($outdated) ? null : $outdated;
} finally {
// Cleanup
if (is_dir($tmpDir)) {
exec(sprintf('rm -rf %s', escapeshellarg($tmpDir)));
}
}
}
// ── npm scanning ─────────────────────────────────────────────────────
private function scanNpm(string $org, string $repoName, bool $patchOnly): ?array
{
// Check if repo has package.json
try {
$this->adapter->getFileContents($org, $repoName, 'package.json');
} catch (\Exception $e) {
return null;
}
// Check for lock file
$hasLock = false;
foreach (['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'] as $lockFile) {
try {
$this->adapter->getFileContents($org, $repoName, $lockFile);
$hasLock = true;
break;
} catch (\Exception $e) {
// continue
}
}
if (!$hasLock) {
return null;
}
$tmpDir = sys_get_temp_dir() . '/moko_deps_npm_' . $repoName . '_' . getmypid();
@mkdir($tmpDir, 0700, true);
try {
$cloneUrl = $this->adapter->getCloneUrl($org, $repoName);
exec(sprintf('git clone --depth 1 --quiet %s %s 2>/dev/null',
escapeshellarg($cloneUrl), escapeshellarg($tmpDir)));
if (!file_exists("{$tmpDir}/package.json")) {
return null;
}
// Install deps first (needed for npm outdated)
exec(sprintf('cd %s && npm install --silent 2>/dev/null', escapeshellarg($tmpDir)));
$json = shell_exec(sprintf('cd %s && npm outdated --json 2>/dev/null', escapeshellarg($tmpDir)));
if ($json === null || $json === '' || $json === '{}') {
return null;
}
$data = json_decode($json, true);
if (!is_array($data) || empty($data)) {
return null;
}
$outdated = [];
foreach ($data as $name => $info) {
$current = $info['current'] ?? '';
$wanted = $info['wanted'] ?? '';
$latest = $info['latest'] ?? '';
$target = $patchOnly ? $wanted : $latest;
if ($current === $target || $target === '') {
continue;
}
$outdated[] = [
'name' => $name,
'current' => $current,
'latest' => $target,
'status' => ($current === $wanted) ? 'up-to-date' : 'outdated',
];
}
return empty($outdated) ? null : $outdated;
} finally {
if (is_dir($tmpDir)) {
exec(sprintf('rm -rf %s', escapeshellarg($tmpDir)));
}
}
}
// ── PR creation ──────────────────────────────────────────────────────
private function cloneUpdateAndPR(
string $org,
string $repoName,
string $branch,
string $title,
string $body,
array $outdated
): int {
$tmpDir = sys_get_temp_dir() . '/moko_deps_pr_' . $repoName . '_' . getmypid();
@mkdir($tmpDir, 0700, true);
try {
$cloneUrl = $this->adapter->getCloneUrl($org, $repoName);
exec(sprintf('git clone --quiet %s %s 2>/dev/null',
escapeshellarg($cloneUrl), escapeshellarg($tmpDir)));
// Create branch
exec(sprintf('git -C %s checkout -b %s 2>/dev/null',
escapeshellarg($tmpDir), escapeshellarg($branch)));
$updated = false;
// Run composer update if needed
if (isset($outdated['composer'])) {
$packages = array_column($outdated['composer'], 'name');
$cmd = sprintf(
'cd %s && composer update %s --no-interaction --quiet 2>/dev/null',
escapeshellarg($tmpDir),
implode(' ', array_map('escapeshellarg', $packages))
);
exec($cmd, $output, $exitCode);
if ($exitCode === 0) {
$updated = true;
}
}
// Run npm update if needed
if (isset($outdated['npm'])) {
$packages = array_column($outdated['npm'], 'name');
$cmd = sprintf(
'cd %s && npm update %s --save 2>/dev/null',
escapeshellarg($tmpDir),
implode(' ', array_map('escapeshellarg', $packages))
);
exec($cmd, $output, $exitCode);
if ($exitCode === 0) {
$updated = true;
}
}
if (!$updated) {
return 0;
}
// Commit and push
exec(sprintf('git -C %s config user.email "gitea-actions[bot]@mokoconsulting.tech"', escapeshellarg($tmpDir)));
exec(sprintf('git -C %s config user.name "gitea-actions[bot]"', escapeshellarg($tmpDir)));
exec(sprintf('git -C %s add -A', escapeshellarg($tmpDir)));
// Check if there are actual changes
exec(sprintf('git -C %s diff --cached --quiet', escapeshellarg($tmpDir)), $output, $diffExit);
if ($diffExit === 0) {
return 0; // No changes
}
exec(sprintf('git -C %s commit -m %s',
escapeshellarg($tmpDir),
escapeshellarg($title . " [skip ci]")));
exec(sprintf('git -C %s push origin %s 2>/dev/null',
escapeshellarg($tmpDir), escapeshellarg($branch)), $output, $pushExit);
if ($pushExit !== 0) {
$this->log(" {$repoName}: push failed", 'ERROR');
return 0;
}
// Create PR via API
$defaultBranch = $this->getDefaultBranch($org, $repoName);
$pr = $this->adapter->createPullRequest(
$org, $repoName, $title, $branch, $defaultBranch, $body, [
'labels' => ['dependencies'],
]
);
return (int) ($pr['number'] ?? 0);
} finally {
if (is_dir($tmpDir)) {
exec(sprintf('rm -rf %s', escapeshellarg($tmpDir)));
}
}
}
// ── Auto-merge ───────────────────────────────────────────────────────
private function tryAutoMerge(string $org, string $repoName, int $prNumber): void
{
try {
$this->api->put(
"/repos/{$org}/{$repoName}/pulls/{$prNumber}/merge",
['Do' => 'squash', 'merge_message_field' => 'chore(deps): auto-merge patch updates']
);
$this->autoMerged++;
$this->log(" {$repoName}: PR #{$prNumber} auto-merged", 'INFO');
} catch (\Exception $e) {
$this->log(" {$repoName}: auto-merge failed — {$e->getMessage()}", 'WARNING');
}
}
// ── Helpers ───────────────────────────────────────────────────────────
private function hasExistingDepsPR(string $org, string $repoName): bool
{
try {
$prs = $this->adapter->listPullRequests($org, $repoName, ['state' => 'open']);
foreach ($prs as $pr) {
if (str_starts_with($pr['head']['ref'] ?? '', self::BRANCH_PREFIX)) {
return true;
}
}
} catch (\Exception $e) {
// Ignore — proceed with creating PR
}
return false;
}
private function getDefaultBranch(string $org, string $repoName): string
{
try {
$repo = $this->api->get("/repos/{$org}/{$repoName}");
return $repo['default_branch'] ?? 'main';
} catch (\Exception $e) {
return 'main';
}
}
private function isPatchUpdate(string $current, string $latest): bool
{
$cur = explode('.', ltrim($current, 'v'));
$lat = explode('.', ltrim($latest, 'v'));
if (count($cur) < 3 || count($lat) < 3) {
return false;
}
// Same major and minor, only patch differs
return $cur[0] === $lat[0] && $cur[1] === $lat[1] && $cur[2] !== $lat[2];
}
private function buildPrBody(string $repoName, array $outdated): string
{
$lines = [
"## Dependency Updates",
"",
"**Repository**: `{$repoName}`",
"**Scanned**: " . date('Y-m-d H:i:s'),
"",
];
foreach ($outdated as $type => $packages) {
$lines[] = "### " . ucfirst($type);
$lines[] = "";
$lines[] = "| Package | Current | Latest | Type |";
$lines[] = "|---------|---------|--------|------|";
foreach ($packages as $pkg) {
$updateType = $this->isPatchUpdate($pkg['current'], $pkg['latest']) ? 'patch' : 'minor/major';
$lines[] = "| `{$pkg['name']}` | {$pkg['current']} | {$pkg['latest']} | {$updateType} |";
}
$lines[] = "";
}
$lines[] = "---";
$lines[] = "*Auto-generated by `moko deps:update`*";
return implode("\n", $lines);
}
}
$script = new UpdateDependencies('update_dependencies', 'Cross-repo dependency update automation');
exit($script->execute());
+7 -13
View File
@@ -9,11 +9,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoCli.CLI * DEFGROUP: MokoStandards.CLI
* INGROUP: MokoCli * INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /bin/moko * PATH: /bin/moko
* BRIEF: Unified CLI dispatcher — run any MokoCli script without needing GitHub Actions * BRIEF: Unified CLI dispatcher — run any MokoStandards script without needing GitHub Actions
* *
* USAGE * USAGE
* php bin/moko <command> [options] (all platforms) * php bin/moko <command> [options] (all platforms)
@@ -89,7 +89,6 @@ const COMMAND_MAP = [
// Automation // Automation
'sync' => 'automation/bulk_sync.php', 'sync' => 'automation/bulk_sync.php',
'deps:update' => 'automation/update_dependencies.php',
'automation:cleanup' => 'automation/repo_cleanup.php', 'automation:cleanup' => 'automation/repo_cleanup.php',
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php', 'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
@@ -178,7 +177,6 @@ const COMMAND_MAP = [
'repo:archive' => 'cli/archive_repo.php', 'repo:archive' => 'cli/archive_repo.php',
'repo:scaffold-client' => 'cli/scaffold_client.php', 'repo:scaffold-client' => 'cli/scaffold_client.php',
'repo:provision' => 'cli/client_provision.php', 'repo:provision' => 'cli/client_provision.php',
'repo:wizard' => 'cli/repo_wizard.php',
'repo:rename-branch' => 'cli/branch_rename.php', 'repo:rename-branch' => 'cli/branch_rename.php',
'repo:reset-dev' => 'cli/dev_branch_reset.php', 'repo:reset-dev' => 'cli/dev_branch_reset.php',
@@ -200,7 +198,6 @@ const COMMAND_MAP = [
'deploy:sftp' => 'deploy/deploy-sftp.php', 'deploy:sftp' => 'deploy/deploy-sftp.php',
'deploy:backup' => 'deploy/backup-before-deploy.php', 'deploy:backup' => 'deploy/backup-before-deploy.php',
'deploy:health-check' => 'deploy/health-check.php', 'deploy:health-check' => 'deploy/health-check.php',
'deploy:verify' => 'deploy/deploy-and-verify.php',
'deploy:rollback' => 'deploy/rollback-joomla.php', 'deploy:rollback' => 'deploy/rollback-joomla.php',
'deploy:sync' => 'deploy/sync-joomla.php', 'deploy:sync' => 'deploy/sync-joomla.php',
@@ -223,9 +220,6 @@ const COMMAND_MAP = [
// Licensing // Licensing
'license' => 'cli/license_manage.php', 'license' => 'cli/license_manage.php',
// Security
'security:advisories' => 'security/advisory_scan.php',
// Shell completion // Shell completion
'completion' => 'cli/completion.php', 'completion' => 'cli/completion.php',
@@ -298,10 +292,10 @@ function printHelp(): void
{ {
echo <<<'HELP' echo <<<'HELP'
╔══════════════════════════════════════════════════════════╗ ╔══════════════════════════════════════════════════════════╗
║ MokoCli CLI (bin/moko) ║ ║ MokoStandards CLI (bin/moko) ║
╚══════════════════════════════════════════════════════════╝ ╚══════════════════════════════════════════════════════════╝
Run any MokoCli script locally without GitHub Actions. Run any MokoStandards script locally without GitHub Actions.
USAGE USAGE
php bin/moko <command> [options] (all platforms) php bin/moko <command> [options] (all platforms)
@@ -403,7 +397,7 @@ function loadPluginCommands(): array
$commands = []; $commands = [];
foreach (glob("{$pluginDir}/*Plugin.php") as $file) { foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
$className = 'MokoCli\\Plugins\\' $className = 'MokoEnterprise\\Plugins\\'
. pathinfo($file, PATHINFO_FILENAME); . pathinfo($file, PATHINFO_FILENAME);
if (!class_exists($className)) { if (!class_exists($className)) {
+8 -8
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/archive_repo.php * PATH: /cli/archive_repo.php
* BRIEF: Gracefully retire a governed repository — archive, close issues/PRs, remove sync def * BRIEF: Gracefully retire a governed repository — archive, close issues/PRs, remove sync def
*/ */
@@ -20,9 +20,9 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\Config; use MokoEnterprise\Config;
use MokoCli\PlatformAdapterFactory; use MokoEnterprise\PlatformAdapterFactory;
class ArchiveRepoCli extends CliFramework class ArchiveRepoCli extends CliFramework
{ {
@@ -135,7 +135,7 @@ class ArchiveRepoCli extends CliFramework
try { try {
$issue = $adapter->createIssue( $issue = $adapter->createIssue(
$org, $org,
'mokocli', 'moko-platform',
"chore: archived repository {$repoName}", "chore: archived repository {$repoName}",
"## Repository Archived\n\n" "## Repository Archived\n\n"
. "**Repository:** `{$org}/{$repoName}`\n" . "**Repository:** `{$org}/{$repoName}`\n"
@@ -150,7 +150,7 @@ class ArchiveRepoCli extends CliFramework
] ]
); );
if (isset($issue['number'])) { if (isset($issue['number'])) {
echo " Archival record: mokocli#{$issue['number']}\n"; echo " Archival record: moko-platform#{$issue['number']}\n";
} }
} catch (\Exception $e) { } catch (\Exception $e) {
echo " Warning: could not create archival record: " . $e->getMessage() . "\n"; echo " Warning: could not create archival record: " . $e->getMessage() . "\n";
+2 -2
View File
@@ -16,7 +16,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.Enterprise.CLI * DEFGROUP: MokoPlatform.Enterprise.CLI
* INGROUP: MokoPlatform.Enterprise * INGROUP: MokoPlatform.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/audit_query.php * PATH: /cli/audit_query.php
* BRIEF: Search, filter, and export audit logs * BRIEF: Search, filter, and export audit logs
*/ */
@@ -25,7 +25,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
/** /**
* CLI tool to search, filter, and export audit logs. * CLI tool to search, filter, and export audit logs.
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/badge_update.php * PATH: /cli/badge_update.php
* BRIEF: Update [VERSION: XX.XX.XX] badges in all markdown files * BRIEF: Update [VERSION: XX.XX.XX] badges in all markdown files
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class BadgeUpdateCli extends CliFramework class BadgeUpdateCli extends CliFramework
{ {
+5 -5
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/branch_rename.php * PATH: /cli/branch_rename.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old) * BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
*/ */
@@ -18,7 +18,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class BranchRenameCli extends CliFramework class BranchRenameCli extends CliFramework
{ {
+7 -7
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/bulk_workflow_push.php * PATH: /cli/bulk_workflow_push.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API * BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class BulkWorkflowPushCli extends CliFramework class BulkWorkflowPushCli extends CliFramework
{ {
@@ -154,7 +154,7 @@ class BulkWorkflowPushCli extends CliFramework
'content' => $encodedContent, 'content' => $encodedContent,
'sha' => $remoteSha, 'sha' => $remoteSha,
'message' => "chore: sync {$destPath} " 'message' => "chore: sync {$destPath} "
. "from mokocli [skip ci]", . "from moko-platform [skip ci]",
'branch' => $branch, 'branch' => $branch,
]); ]);
@@ -184,7 +184,7 @@ class BulkWorkflowPushCli extends CliFramework
$payload = json_encode([ $payload = json_encode([
'content' => $encodedContent, 'content' => $encodedContent,
'message' => "chore: add {$destPath} " 'message' => "chore: add {$destPath} "
. "from mokocli [skip ci]", . "from moko-platform [skip ci]",
'branch' => $branch, 'branch' => $branch,
]); ]);
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/bulk_workflow_trigger.php * PATH: /cli/bulk_workflow_trigger.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Trigger a workflow across multiple repos at once * BRIEF: Trigger a workflow across multiple repos at once
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class BulkWorkflowTriggerCli extends CliFramework class BulkWorkflowTriggerCli extends CliFramework
{ {
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/changelog_promote.php * PATH: /cli/changelog_promote.php
* BRIEF: Promote [Unreleased] section in CHANGELOG.md to a versioned entry * BRIEF: Promote [Unreleased] section in CHANGELOG.md to a versioned entry
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ChangelogPromoteCli extends CliFramework class ChangelogPromoteCli extends CliFramework
{ {
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/changelog_prune.php * PATH: /cli/changelog_prune.php
* BRIEF: Prune old CHANGELOG.md entries — keeps [Unreleased] + last N releases * BRIEF: Prune old CHANGELOG.md entries — keeps [Unreleased] + last N releases
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ChangelogPruneCli extends CliFramework class ChangelogPruneCli extends CliFramework
{ {
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/client_dashboard.php * PATH: /cli/client_dashboard.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Generate unified client dashboard HTML * BRIEF: Generate unified client dashboard HTML
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ClientDashboardCli extends CliFramework class ClientDashboardCli extends CliFramework
{ {
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/client_health_check.php * PATH: /cli/client_health_check.php
* BRIEF: Verify a client site's update server, installed version, and release availability * BRIEF: Verify a client site's update server, installed version, and release availability
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ClientHealthCheckCli extends CliFramework class ClientHealthCheckCli extends CliFramework
{ {
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/client_inventory.php * PATH: /cli/client_inventory.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Discover and list all client-waas repos with their server configuration status * BRIEF: Discover and list all client-waas repos with their server configuration status
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ClientInventoryCli extends CliFramework class ClientInventoryCli extends CliFramework
{ {
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/client_provision.php * PATH: /cli/client_provision.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Provision a new client environment end-to-end * BRIEF: Provision a new client environment end-to-end
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ClientProvisionCli extends CliFramework class ClientProvisionCli extends CliFramework
{ {
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/completion.php * PATH: /cli/completion.php
* BRIEF: Generate bash/zsh tab completion scripts for bin/moko * BRIEF: Generate bash/zsh tab completion scripts for bin/moko
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class CompletionCli extends CliFramework class CompletionCli extends CliFramework
{ {
+12 -12
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/create_project.php * PATH: /cli/create_project.php
* BRIEF: Create baseline GitHub Projects for repositories with standard fields and views * BRIEF: Create baseline GitHub Projects for repositories with standard fields and views
*/ */
@@ -19,12 +19,12 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class CreateProjectCli extends CliFramework class CreateProjectCli extends CliFramework
{ {
/** @var string[] */ /** @var string[] */
private array $ALWAYS_EXCLUDE = ['mokocli', '.github-private']; private array $ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
/** @var array<string, string> */ /** @var array<string, string> */
private array $PLATFORM_TO_TYPE = [ private array $PLATFORM_TO_TYPE = [
@@ -80,10 +80,10 @@ class CreateProjectCli extends CliFramework
return 2; return 2;
} }
$config = \MokoCli\Config::load(); $config = \MokoEnterprise\Config::load();
$platformName = $config->getString('platform', 'gitea'); $platformName = $config->getString('platform', 'gitea');
try { try {
$adapter = \MokoCli\PlatformAdapterFactory::create($config); $adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$api = $adapter->getApiClient(); $api = $adapter->getApiClient();
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage()); $this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
@@ -183,7 +183,7 @@ class CreateProjectCli extends CliFramework
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
'Authorization: bearer ' . $token, 'Authorization: bearer ' . $token,
'Content-Type: application/json', 'Content-Type: application/json',
'User-Agent: mokocli-CreateProject', 'User-Agent: moko-platform-CreateProject',
], ],
]); ]);
$body = (string) curl_exec($ch); $body = (string) curl_exec($ch);
@@ -205,7 +205,7 @@ class CreateProjectCli extends CliFramework
return $data['data'] ?? []; return $data['data'] ?? [];
} }
private function restGet(string $path, string $token, ?\MokoCli\ApiClient $apiClient = null): array private function restGet(string $path, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): array
{ {
if ($apiClient !== null) { if ($apiClient !== null) {
try { try {
@@ -217,7 +217,7 @@ class CreateProjectCli extends CliFramework
return []; return [];
} }
private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoCli\ApiClient $apiClient = null): string private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string
{ {
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) { foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient); $data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
@@ -422,14 +422,14 @@ class CreateProjectCli extends CliFramework
updateProjectV2(input: { updateProjectV2(input: {
projectId: $projectId, projectId: $projectId,
shortDescription: $shortDescription, shortDescription: $shortDescription,
readme: "Managed by mokocli. Run `php cli/create_project.php` to regenerate." readme: "Managed by moko-platform. Run `php cli/create_project.php` to regenerate."
}) { }) {
projectV2 { id } projectV2 { id }
} }
}', }',
[ [
'projectId' => $projectId, 'projectId' => $projectId,
'shortDescription' => "Standard project board for {$repo}. Auto-created by mokocli.", 'shortDescription' => "Standard project board for {$repo}. Auto-created by moko-platform.",
], ],
$token $token
); );
+21 -21
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/create_repo.php * PATH: /cli/create_repo.php
* BRIEF: Scaffold a new governed repository with full mokocli baseline * BRIEF: Scaffold a new governed repository with full moko-platform baseline
*/ */
declare(strict_types=1); declare(strict_types=1);
@@ -20,15 +20,15 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\Config; use MokoEnterprise\Config;
use MokoCli\PlatformAdapterFactory; use MokoEnterprise\PlatformAdapterFactory;
class CreateRepoCli extends CliFramework class CreateRepoCli extends CliFramework
{ {
protected function configure(): void protected function configure(): void
{ {
$this->setDescription('Scaffold a new governed repository with full mokocli baseline'); $this->setDescription('Scaffold a new governed repository with full moko-platform baseline');
$this->addArgument('--name', 'Repository name', null); $this->addArgument('--name', 'Repository name', null);
$this->addArgument('--type', 'Project type', null); $this->addArgument('--type', 'Project type', null);
$this->addArgument('--description', 'Repository description', ''); $this->addArgument('--description', 'Repository description', '');
@@ -60,16 +60,16 @@ class CreateRepoCli extends CliFramework
'generic' => 'generic', 'generic' => 'generic',
]; ];
$TYPE_TO_TOPICS = [ $TYPE_TO_TOPICS = [
'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'mokocli'], 'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'moko-platform'],
'joomla' => ['joomla', 'cms', 'php', 'mokocli'], 'joomla' => ['joomla', 'cms', 'php', 'moko-platform'],
'nodejs' => ['nodejs', 'javascript', 'typescript', 'mokocli'], 'nodejs' => ['nodejs', 'javascript', 'typescript', 'moko-platform'],
'terraform' => ['terraform', 'infrastructure', 'iac', 'mokocli'], 'terraform' => ['terraform', 'infrastructure', 'iac', 'moko-platform'],
'python' => ['python', 'mokocli'], 'python' => ['python', 'moko-platform'],
'wordpress' => ['wordpress', 'php', 'cms', 'mokocli'], 'wordpress' => ['wordpress', 'php', 'cms', 'moko-platform'],
'generic' => ['mokocli'], 'generic' => ['moko-platform'],
]; ];
$platform = $TYPE_TO_PLATFORM[$type] ?? 'generic'; $platform = $TYPE_TO_PLATFORM[$type] ?? 'generic';
$topics = $TYPE_TO_TOPICS[$type] ?? ['mokocli']; $topics = $TYPE_TO_TOPICS[$type] ?? ['moko-platform'];
$platformName = $adapter->getPlatformName(); $platformName = $adapter->getPlatformName();
$vis = $private ? 'private' : 'public'; $vis = $private ? 'private' : 'public';
echo "Scaffolding new repository: {$org}/{$name}" echo "Scaffolding new repository: {$org}/{$name}"
@@ -84,7 +84,7 @@ class CreateRepoCli extends CliFramework
if (!$this->dryRun) { if (!$this->dryRun) {
try { try {
$data = $adapter->createOrgRepo($org, $name, [ $data = $adapter->createOrgRepo($org, $name, [
'description' => $description ?: "Managed by mokocli ({$type})", 'description' => $description ?: "Managed by moko-platform ({$type})",
'private' => $private, 'private' => $private,
'has_issues' => true, 'has_issues' => true,
'has_projects' => true, 'has_projects' => true,
@@ -138,12 +138,12 @@ class CreateRepoCli extends CliFramework
echo "Step 4: Creating README.md...\n"; echo "Step 4: Creating README.md...\n";
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com'; $baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
$repoUrl = "{$baseUrl}/{$org}/{$name}"; $repoUrl = "{$baseUrl}/{$org}/{$name}";
$standardsUrl = "{$baseUrl}/{$org}/MokoCli"; $standardsUrl = "{$baseUrl}/{$org}/MokoStandards";
$readmeContent = "<!--\n" $readmeContent = "<!--\n"
. "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n" . "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n"
. "SPDX-License-Identifier: GPL-3.0-or-later\n" . "SPDX-License-Identifier: GPL-3.0-or-later\n"
. "DEFGROUP: {$name}\n" . "DEFGROUP: {$name}\n"
. "INGROUP: mokocli\n" . "INGROUP: moko-platform\n"
. "REPO: {$repoUrl}\n" . "REPO: {$repoUrl}\n"
. "PATH: /README.md\n" . "PATH: /README.md\n"
. "BRIEF: {$description}\n" . "BRIEF: {$description}\n"
@@ -152,7 +152,7 @@ class CreateRepoCli extends CliFramework
. "{$description}\n\n" . "{$description}\n\n"
. "## Getting Started\n\n" . "## Getting Started\n\n"
. "This repository is governed by" . "This repository is governed by"
. " [mokocli]({$standardsUrl}).\n\n" . " [moko-platform]({$standardsUrl}).\n\n"
. "## License\n\n" . "## License\n\n"
. "GPL-3.0-or-later. See [LICENSE](LICENSE)" . "GPL-3.0-or-later. See [LICENSE](LICENSE)"
. " for details.\n"; . " for details.\n";
@@ -169,7 +169,7 @@ class CreateRepoCli extends CliFramework
$name, $name,
'README.md', 'README.md',
$readmeContent, $readmeContent,
'docs: initialize README with mokocli header [skip ci]', 'docs: initialize README with moko-platform header [skip ci]',
$sha $sha
); );
echo " README.md created\n"; echo " README.md created\n";
+7 -7
View File
@@ -10,7 +10,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoPlatform.CLI * DEFGROUP: MokoPlatform.CLI
* INGROUP: MokoPlatform * INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/deploy_joomla.php * PATH: /cli/deploy_joomla.php
* BRIEF: Smart Joomla deploy — routes files to correct server directories by extension type * BRIEF: Smart Joomla deploy — routes files to correct server directories by extension type
* *
@@ -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 MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
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 (source/ → src/ → htdocs/) // 3-5. Fallback chain
$resolved = SourceResolver::resolveAbsolute($repoPath); foreach (['src', 'htdocs'] as $candidate) {
if ($resolved !== null) { if (is_dir("{$repoPath}/{$candidate}")) {
SourceResolver::warnIfLegacy($repoPath); return "{$repoPath}/{$candidate}";
return $resolved; }
} }
// Last resort: repo root itself // Last resort: repo root itself
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/dev_branch_reset.php * PATH: /cli/dev_branch_reset.php
* BRIEF: Delete and recreate dev branch from main via Gitea API * BRIEF: Delete and recreate dev branch from main via Gitea API
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class DevBranchResetCli extends CliFramework class DevBranchResetCli extends CliFramework
{ {
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/grafana_dashboard.php * PATH: /cli/grafana_dashboard.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Manage Grafana dashboards via API * BRIEF: Manage Grafana dashboards via API
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class GrafanaDashboardCli extends CliFramework class GrafanaDashboardCli extends CliFramework
{ {
+13 -8
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/joomla_build.php * PATH: /cli/joomla_build.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported * BRIEF: Build a Joomla extension ZIP from manifest — all types supported
* NOTE: Called by pre-release and auto-release workflows. * NOTE: Called by pre-release and auto-release workflows.
*/ */
@@ -19,7 +19,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
class JoomlaBuildCli extends CliFramework class JoomlaBuildCli extends CliFramework
{ {
@@ -49,12 +49,17 @@ class JoomlaBuildCli extends CliFramework
$path = realpath($path) ?: $path; $path = realpath($path) ?: $path;
// ── Find source directory ────────────────────────────────────────────── // ── Find source directory ──────────────────────────────────────────────
$srcDir = SourceResolver::resolveAbsolute($path); $srcDir = null;
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 source/ or src/ directory in {$path}"); $this->log('ERROR', "::error::No src/ or htdocs/ directory in {$path}");
return 1; return 1;
} }
SourceResolver::warnIfLegacy($path);
// ── Find manifest ────────────────────────────────────────────────────── // ── Find manifest ──────────────────────────────────────────────────────
$manifest = $this->findManifest($srcDir); $manifest = $this->findManifest($srcDir);
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/joomla_compat_check.php * PATH: /cli/joomla_compat_check.php
* BRIEF: Check if extension targetplatform regex matches the latest Joomla version * BRIEF: Check if extension targetplatform regex matches the latest Joomla version
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class JoomlaCompatCheckCli extends CliFramework class JoomlaCompatCheckCli extends CliFramework
{ {
-507
View File
@@ -1,507 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/joomla_metadata_validate.php
* VERSION: 09.38.00
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
class JoomlaMetadataValidateCli extends CliFramework
{
/** Joomla element prefix map — must match MokoGitea's cleanJoomlaElement() */
private const JOOMLA_PREFIX = [
'package' => 'pkg_',
'component' => 'com_',
'module' => 'mod_',
'template' => 'tpl_',
'library' => 'lib_',
'file' => 'file_',
];
protected function configure(): void
{
$this->setDescription('Validate MokoGitea repo metadata against Joomla extension manifest XML');
$this->addArgument('--path', 'Repo root path (default: current directory)', '.');
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
$this->addArgument('--repo', 'Repo name (auto-detected from git if empty)', '');
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--ci', 'CI mode: exit 1 on any error', false);
$this->addArgument('--json', 'Output as JSON', false);
}
protected function run(): int
{
$path = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
$org = $this->getArgument('--org');
$repoName = $this->getArgument('--repo');
$apiBase = rtrim($this->getArgument('--api-base'), '/');
$ciMode = (bool) $this->getArgument('--ci');
$jsonMode = (bool) $this->getArgument('--json');
if (!is_dir($path)) {
$this->log('ERROR', "Path does not exist: {$path}");
return 1;
}
if ($repoName === '') {
$repoName = $this->detectRepoName($path);
}
// ── Step 1: Find the Joomla extension manifest XML ──────────
$joomlaXml = $this->findJoomlaManifest($path);
if ($joomlaXml === null) {
$this->log('ERROR', 'No Joomla extension manifest XML found');
return 1;
}
$this->log('INFO', "Joomla manifest: {$joomlaXml['path']}");
// ── Step 2: Load MokoGitea metadata ─────────────────────────
$metadata = $this->loadMetadata($path, $org, $repoName, $token, $apiBase);
if ($metadata === null) {
$this->log('ERROR', 'Could not load MokoGitea metadata');
return 1;
}
// ── Step 3: Compare ─────────────────────────────────────────
$results = $this->compare($metadata, $joomlaXml, $path);
// ── Step 4: Output ──────────────────────────────────────────
if ($jsonMode) {
echo json_encode([
'repo' => $repoName,
'results' => $results,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
} else {
$this->printResults($repoName, $results);
}
$errors = count(array_filter($results, fn($r) => $r['status'] === 'error'));
return ($ciMode && $errors > 0) ? 1 : 0;
}
// =================================================================
// Find Joomla manifest XML
// =================================================================
private function findJoomlaManifest(string $root): ?array
{
// Search common locations for a Joomla extension manifest
$candidates = [];
// Package manifest: source/pkg_*.xml
foreach (glob("{$root}/source/pkg_*.xml") as $file) {
$candidates[] = $file;
}
// Component manifest: source/packages/com_*/[name].xml
foreach (glob("{$root}/source/packages/com_*/*.xml") as $file) {
$basename = basename($file);
// Skip access.xml, config.xml, etc.
if (in_array($basename, ['access.xml', 'config.xml'], true)) {
continue;
}
$candidates[] = $file;
}
// Direct source/*.xml
foreach (glob("{$root}/source/*.xml") as $file) {
if (basename($file) !== 'pkg_mokosuitebackup.xml') {
// Already caught above
}
$candidates[] = $file;
}
// src/ fallback
foreach (glob("{$root}/src/pkg_*.xml") as $file) {
$candidates[] = $file;
}
// Find the first one that has <extension type="...">
foreach (array_unique($candidates) as $file) {
$content = file_get_contents($file);
if ($content === false) {
continue;
}
if (preg_match('/<extension\s[^>]*type=["\']([^"\']+)["\']/', $content, $typeMatch)) {
$xml = @simplexml_load_string($content);
if ($xml === false) {
$relPath = str_replace($root . '/', '', $file);
$relPath = str_replace($root . '\\', '', $relPath);
$this->log('WARN', "Skipping {$relPath}: malformed XML");
continue;
}
$type = strtolower($typeMatch[1]);
$relPath = str_replace($root . '/', '', $file);
$relPath = str_replace($root . '\\', '', $relPath);
return [
'path' => $relPath,
'type' => $type,
'xml' => $xml,
];
}
}
return null;
}
// =================================================================
// Load metadata (from API)
// =================================================================
private function loadMetadata(string $root, string $org, string $repoName, string $token, string $apiBase): ?array
{
if ($token === '') {
$this->log('ERROR', 'No API token provided (use --token or set GITEA_TOKEN env var)');
return null;
}
$url = "{$apiBase}/repos/{$org}/{$repoName}/metadata";
$ctx = stream_context_create([
'http' => [
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
'timeout' => 10,
'ignore_errors' => true,
],
]);
$body = file_get_contents($url, false, $ctx);
// Extract HTTP status from response headers
$httpCode = 0;
if (isset($http_response_header[0]) && preg_match('/\d{3}/', $http_response_header[0], $m)) {
$httpCode = (int) $m[0];
}
if ($body === false) {
$this->log('ERROR', "Failed to connect to {$url} — check network or TLS configuration");
return null;
}
if ($httpCode === 404) {
$this->log('ERROR', "API endpoint not found: {$url}");
$this->log('ERROR', 'Server may need MokoGitea-Fork >= #650 (metadata endpoint rename)');
return null;
}
if ($httpCode === 401 || $httpCode === 403) {
$this->log('ERROR', "Authentication failed (HTTP {$httpCode}) — check your API token");
return null;
}
if ($httpCode >= 400) {
$this->log('ERROR', "API returned HTTP {$httpCode}: " . substr($body, 0, 200));
return null;
}
$data = json_decode($body, true);
if (!is_array($data)) {
$this->log('ERROR', "API returned invalid JSON from {$url}");
return null;
}
$data['source'] = 'api';
return $data;
}
// =================================================================
// Compare metadata against Joomla manifest
// =================================================================
private function compare(array $metadata, array $joomlaXml, string $root): array
{
$results = [];
$xml = $joomlaXml['xml'];
$type = $joomlaXml['type'];
// 1. Extension type
$metaType = $this->normalizeExtensionType(
$metadata['extension_type'] ?? $metadata['package_type'] ?? ''
);
$results[] = [
'field' => 'extension_type',
'metadata' => $metaType,
'joomla' => $type,
'status' => ($metaType === $type) ? 'ok' : 'error',
'message' => ($metaType === $type)
? "matches <extension type=\"{$type}\">"
: "metadata has \"{$metaType}\" but Joomla manifest has \"{$type}\"",
];
// 2. Element name
$metaName = strtolower($metadata['name'] ?? '');
$metaElement = $this->deriveElement($metaType, $metaName);
$joomlaElement = $this->extractJoomlaElement($xml, $type);
$elementMatch = ($metaElement === $joomlaElement);
$results[] = [
'field' => 'element',
'metadata' => $metaElement,
'joomla' => $joomlaElement,
'status' => $elementMatch ? 'ok' : 'error',
'message' => $elementMatch
? "derived correctly"
: "metadata derives \"{$metaElement}\" but Joomla uses \"{$joomlaElement}\"",
];
// 3. Version
$metaVersion = $metadata['version'] ?? '';
$joomlaVersion = (string) ($xml->version ?? '');
if ($metaVersion !== '' && $joomlaVersion !== '') {
// Strip dev/rc suffixes for comparison (CI bumps these)
$metaBase = preg_replace('/-(dev|rc|alpha|beta)\d*$/', '', $metaVersion);
$joomlaBase = preg_replace('/-(dev|rc|alpha|beta)\d*$/', '', $joomlaVersion);
$versionMatch = ($metaBase === $joomlaBase);
$results[] = [
'field' => 'version',
'metadata' => $metaVersion,
'joomla' => $joomlaVersion,
'status' => $versionMatch ? 'ok' : 'warn',
'message' => $versionMatch
? 'matches (base version)'
: "metadata has \"{$metaVersion}\" but Joomla has \"{$joomlaVersion}\"",
];
}
// 4. PHP minimum (from composer.json)
$composerPhp = $this->readComposerPhpRequirement($root);
$metaPhp = $metadata['php_minimum'] ?? '';
if ($composerPhp !== '' && $metaPhp !== '') {
$phpMatch = ($metaPhp === $composerPhp);
$results[] = [
'field' => 'php_minimum',
'metadata' => $metaPhp,
'joomla' => $composerPhp . ' (composer.json)',
'status' => $phpMatch ? 'ok' : 'warn',
'message' => $phpMatch
? 'matches composer.json'
: "metadata has \"{$metaPhp}\" but composer.json requires \"{$composerPhp}\"",
];
}
// 5. Description
$metaDesc = $metadata['description'] ?? '';
$joomlaDesc = (string) ($xml->description ?? '');
// Joomla descriptions are often language keys, skip those
if ($metaDesc !== '' && $joomlaDesc !== '' && !str_starts_with($joomlaDesc, 'COM_') && !str_starts_with($joomlaDesc, 'PKG_')) {
$descMatch = ($metaDesc === $joomlaDesc);
$results[] = [
'field' => 'description',
'metadata' => substr($metaDesc, 0, 60) . (strlen($metaDesc) > 60 ? '...' : ''),
'joomla' => substr($joomlaDesc, 0, 60) . (strlen($joomlaDesc) > 60 ? '...' : ''),
'status' => $descMatch ? 'ok' : 'info',
'message' => $descMatch ? 'matches' : 'descriptions differ (informational)',
];
}
return $results;
}
// =================================================================
// Helpers
// =================================================================
/**
* Normalize extension_type — map MokoGitea types to Joomla types.
*/
private function normalizeExtensionType(string $type): string
{
return match (strtolower($type)) {
'joomla-extension' => 'package', // legacy mapping
default => strtolower($type),
};
}
/**
* Derive the Joomla element name from type + name.
* Replicates MokoGitea's cleanJoomlaElement() + prefix logic.
*/
private function deriveElement(string $type, string $name): string
{
// Clean: lowercase, strip non-alphanumeric except . _ -
$clean = strtolower($name);
$clean = preg_replace('/[^a-z0-9._-]/', '', $clean);
$prefix = self::JOOMLA_PREFIX[$type] ?? '';
return $prefix . $clean;
}
/**
* Extract the element name from a Joomla manifest XML.
* Follows the same logic as Joomla's InstallerAdapter::getElement().
*/
private function extractJoomlaElement(\SimpleXMLElement $xml, string $type): string
{
switch ($type) {
case 'package':
$packagename = (string) ($xml->packagename ?? '');
if ($packagename !== '') {
return 'pkg_' . strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $packagename));
}
break;
case 'component':
$element = (string) ($xml->element ?? '');
if ($element !== '') {
$element = strtolower($element);
return str_starts_with($element, 'com_') ? $element : 'com_' . $element;
}
$name = (string) ($xml->name ?? '');
$name = strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $name));
return str_starts_with($name, 'com_') ? $name : 'com_' . $name;
case 'module':
$element = (string) ($xml->element ?? '');
if ($element !== '') {
return strtolower($element);
}
break;
case 'plugin':
// Plugins derive element from the file attribute
if (isset($xml->files)) {
foreach ($xml->files->children() as $file) {
$plugin = (string) ($file->attributes()->plugin ?? '');
if ($plugin !== '') {
return strtolower($plugin);
}
}
}
break;
case 'library':
$libname = (string) ($xml->libraryname ?? '');
if ($libname !== '') {
return strtolower($libname);
}
break;
}
// Fallback: use <name> tag
$name = (string) ($xml->name ?? '');
return strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $name));
}
/**
* Read PHP version requirement from composer.json.
*/
private function readComposerPhpRequirement(string $root): string
{
$composerFile = "{$root}/composer.json";
if (!is_file($composerFile)) {
return '';
}
$data = json_decode(file_get_contents($composerFile), true);
if (!is_array($data)) {
return '';
}
$phpReq = $data['require']['php'] ?? '';
// Extract version number from constraint like ">=8.1"
if (preg_match('/(\d+\.\d+)/', $phpReq, $m)) {
return $m[1];
}
return '';
}
private function detectRepoName(string $root): string
{
$gitConfig = "{$root}/.git/config";
if (!file_exists($gitConfig)) {
return basename($root);
}
$content = file_get_contents($gitConfig);
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
return $m[1];
}
return basename($root);
}
// =================================================================
// Output
// =================================================================
private function printResults(string $repoName, array $results): void
{
$errors = count(array_filter($results, fn($r) => $r['status'] === 'error'));
$warns = count(array_filter($results, fn($r) => $r['status'] === 'warn'));
$oks = count(array_filter($results, fn($r) => $r['status'] === 'ok'));
$this->log('INFO', "Validating {$repoName} Joomla metadata...\n");
foreach ($results as $r) {
$icon = match ($r['status']) {
'ok' => "\xE2\x9C\x93", // ✓
'error' => "\xE2\x9C\x97", // ✗
'warn' => "\xE2\x9A\xA0", // ⚠
default => "\xE2\x84\xB9", //
};
$line = sprintf(
" %s %-16s %s",
$icon,
$r['field'],
$r['message']
);
$this->log(
match ($r['status']) {
'error' => 'ERROR',
'warn' => 'WARN',
'ok' => 'OK',
default => 'INFO',
},
$line
);
}
echo "\n";
if ($errors > 0) {
$this->log('ERROR', "{$errors} error(s) — update delivery will fail");
} elseif ($warns > 0) {
$this->log('WARN', "All critical checks passed, {$warns} warning(s)");
} else {
$this->log('OK', "All {$oks} checks passed");
}
}
}
$app = new JoomlaMetadataValidateCli();
exit($app->execute());
+8 -9
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/joomla_release.php * PATH: /cli/joomla_release.php
* BRIEF: Joomla release pipeline — build ZIP+tar.gz, upload to GitHub Release, update updates.xml * BRIEF: Joomla release pipeline — build ZIP+tar.gz, upload to GitHub Release, update updates.xml
* *
@@ -25,7 +25,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver}; use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
/** /**
* Joomla Release Manager * Joomla Release Manager
@@ -57,7 +57,7 @@ class JoomlaRelease extends CliFramework
]; ];
private ApiClient $api; private ApiClient $api;
private \MokoCli\GitPlatformAdapter $adapter; private \MokoEnterprise\GitPlatformAdapter $adapter;
protected function configure(): void protected function configure(): void
{ {
@@ -121,12 +121,11 @@ 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 = SourceResolver::resolveAbsolute($path); $srcDir = is_dir("{$path}/src") ? "{$path}/src" : (is_dir("{$path}/htdocs") ? "{$path}/htdocs" : null);
if ($srcDir === null) { if ($srcDir === null) {
$this->log('ERROR', 'No source/ or src/ directory'); $this->log('ERROR', 'No src/ or htdocs/ 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";
@@ -407,7 +406,7 @@ class JoomlaRelease extends CliFramework
$this->api->post("/repos/{$repo}/releases", [ $this->api->post("/repos/{$repo}/releases", [
'tag_name' => $tag, 'tag_name' => $tag,
'name' => $releaseName, 'name' => $releaseName,
'body' => "## {$version}\n\nCreated by mokocli release pipeline.", 'body' => "## {$version}\n\nCreated by moko-platform release pipeline.",
'prerelease' => ($stability !== 'stable'), 'prerelease' => ($stability !== 'stable'),
]); ]);
} }
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/license_manage.php * PATH: /cli/license_manage.php
* BRIEF: Manage license packages and keys via MokoGitea licensing API * BRIEF: Manage license packages and keys via MokoGitea licensing API
* *
@@ -28,7 +28,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class LicenseManage extends CliFramework class LicenseManage extends CliFramework
{ {
-749
View File
@@ -1,749 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_detect.php
* VERSION: 09.38.00
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
class ManifestDetectCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Auto-detect manifest fields from source files');
$this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--json', 'Output as JSON', false);
$this->addArgument('--diff', 'Show diff against current manifest API values', false);
$this->addArgument('--update', 'Push detected fields to manifest API', false);
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
$this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', '');
$this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false);
}
protected function run(): int
{
$path = $this->getArgument('--path');
$jsonMode = (bool) $this->getArgument('--json');
$diffMode = (bool) $this->getArgument('--diff');
$updateMode = (bool) $this->getArgument('--update');
$ghOutput = (bool) $this->getArgument('--github-output');
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
$apiBase = rtrim($this->getArgument('--api-base'), '/');
$org = $this->getArgument('--org');
$repoName = $this->getArgument('--repo');
$root = realpath($path) ?: $path;
if (!is_dir($root)) {
$this->log('ERROR', "Path does not exist: {$path}");
return 1;
}
// Auto-detect repo name from git remote
if ($repoName === '') {
$repoName = $this->detectRepoName($root);
}
// ── Detect all fields ───────────────────────────────────────
$detected = $this->detectAll($root, $repoName);
// ── Warn about missing fields ────────────────────────────────
$expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
foreach ($expected as $field) {
if (!isset($detected[$field]) || $detected[$field] === '') {
$this->log('WARN', "Could not detect: {$field}");
}
}
// ── Output ──────────────────────────────────────────────────
if ($diffMode || $updateMode) {
if ($token === '') {
$this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)');
return 1;
}
if ($repoName === '') {
$this->log('ERROR', 'Could not determine repo name (use --repo)');
return 1;
}
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
if ($current === null) {
$this->log('ERROR', 'Failed to fetch current manifest from API');
return 1;
}
$changes = $this->computeDiff($current, $detected);
if ($diffMode) {
if (empty($changes)) {
$this->log('INFO', 'No differences — manifest matches source');
} else {
$this->sectionHeader('Manifest Drift');
foreach ($changes as $field => $info) {
$this->log('WARN', sprintf(
'%-20s API: %-30s Detected: %s',
$field,
$info['current'] === '' ? '(empty)' : $info['current'],
$info['detected']
));
}
}
}
if ($updateMode) {
if (empty($changes)) {
$this->log('INFO', 'Nothing to update');
} else {
$update = array_map(fn($i) => $i['detected'], $changes);
$ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update);
if ($ok) {
$this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update)));
} else {
$this->log('ERROR', 'Failed to push manifest update');
return 1;
}
}
}
return 0;
}
if ($ghOutput) {
$outputFile = getenv('GITHUB_OUTPUT');
$lines = [];
foreach ($detected as $k => $v) {
$envKey = str_replace('-', '_', $k);
$lines[] = "{$envKey}={$v}";
}
if ($outputFile !== false && $outputFile !== '') {
file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND);
$this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT');
} else {
$this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead');
echo implode("\n", $lines) . "\n";
}
return 0;
}
if ($jsonMode) {
echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
} else {
foreach ($detected as $k => $v) {
echo "{$k}={$v}\n";
}
}
return 0;
}
// =====================================================================
// Detection engine
// =====================================================================
private function detectAll(string $root, string $repoName): array
{
$platform = $this->detectPlatform($root);
$fields = [
'platform' => $platform,
'name' => '',
'description' => '',
'version' => '',
'element_name' => '',
'package_type' => '',
'language' => '',
'entry_point' => '',
'license_spdx' => '',
'display_name' => '',
'target_version' => '',
'php_minimum' => '',
];
switch ($platform) {
case 'joomla':
$this->detectJoomla($root, $repoName, $fields);
break;
case 'dolibarr':
$this->detectDolibarr($root, $repoName, $fields);
break;
case 'go':
$this->detectGo($root, $repoName, $fields);
break;
case 'mcp':
$this->detectNode($root, $repoName, $fields);
break;
case 'node':
$this->detectNode($root, $repoName, $fields);
$fields['platform'] = 'node';
break;
default:
$this->detectGeneric($root, $repoName, $fields);
break;
}
// Fallbacks
if ($fields['name'] === '') {
$fields['name'] = $repoName ?: basename($root);
}
if ($fields['entry_point'] === '') {
$fields['entry_point'] = $this->detectEntryPoint($root);
}
if ($fields['license_spdx'] === '') {
$fields['license_spdx'] = $this->detectLicense($root);
}
// description: only from platform-specific source, never guessed
// Strip empty values
return array_filter($fields, fn($v) => $v !== '');
}
// ── Platform detection ──────────────────────────────────────────
private function detectPlatform(string $root): string
{
// Joomla: look for pkg_*.xml or extension XML in source dirs
$joomlaXmls = array_merge(
SourceResolver::globSource($root, 'pkg_*.xml'),
glob("{$root}/pkg_*.xml") ?: []
);
if (!empty($joomlaXmls)) {
return 'joomla';
}
// Check source dirs for any Joomla extension XML
foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) {
$content = file_get_contents($xmlFile);
if (strpos($content, '<extension') !== false) {
return 'joomla';
}
}
// Dolibarr: mod*.class.php with DolibarrModules
$modFiles = array_merge(
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
glob("{$root}/core/modules/mod*.class.php") ?: []
);
foreach ($modFiles as $file) {
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
return 'dolibarr';
}
}
// Go
if (file_exists("{$root}/go.mod")) {
return 'go';
}
// MCP: package.json with mcp-related content
if (file_exists("{$root}/package.json")) {
$pkg = json_decode(file_get_contents("{$root}/package.json"), true) ?? [];
$deps = array_merge(
array_keys($pkg['dependencies'] ?? []),
array_keys($pkg['devDependencies'] ?? [])
);
foreach ($deps as $dep) {
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
return 'mcp';
}
}
return 'node';
}
// Python
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
return 'python';
}
return 'generic';
}
// ── Joomla ──────────────────────────────────────────────────────
private function detectJoomla(string $root, string $repoName, array &$fields): void
{
$fields['language'] = 'PHP';
// Find the primary extension manifest XML
$extManifest = $this->findJoomlaManifest($root);
if ($extManifest === null) {
return;
}
$xml = file_get_contents($extManifest);
// Type
$extType = '';
if (preg_match('/type="([^"]*)"/', $xml, $m)) {
$extType = $m[1];
}
$fields['package_type'] = $extType;
// Element name
$element = '';
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
$element = $m[1];
}
if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) {
$element = $m[1];
}
if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) {
$element = $m[1];
}
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $m)) {
$element = $m[1];
}
if ($element === '') {
$element = strtolower(basename($extManifest, '.xml'));
}
// Ensure element has type prefix (API stores full element_name like pkg_mokosuite)
$prefixMap = [
'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_',
'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_',
];
if (isset($prefixMap[$extType])) {
$prefix = $prefixMap[$extType];
// Only add prefix if not already present (check all known prefixes)
$hasPrefix = false;
foreach ($prefixMap as $p) {
if (strpos($element, $p) === 0) { $hasPrefix = true; break; }
}
if (strpos($element, 'plg_') === 0) { $hasPrefix = true; }
if (!$hasPrefix) {
$element = $prefix . $element;
}
} elseif ($extType === 'plugin') {
$folder = '';
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
$folder = $gm[1];
}
if ($folder !== '' && strpos($element, 'plg_') !== 0) {
$element = "plg_{$folder}_" . $element;
}
}
$fields['element_name'] = $element;
// Name
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
$fields['name'] = trim($m[1]);
}
// Version
if (preg_match('/<version>([^<]+)<\/version>/', $xml, $m)) {
$fields['version'] = trim($m[1]);
}
// Description
if (preg_match('/<description>([^<]+)<\/description>/', $xml, $m)) {
$desc = trim($m[1]);
// Skip language string keys like COM_MOKOSUITE_DESCRIPTION
if (strpos($desc, '_') === false || strlen($desc) > 60) {
$fields['description'] = $desc;
}
}
// Display name for update feeds
if (!empty($fields['name'])) {
$name = $fields['name'];
// If name already has "Type - " prefix, use as-is
if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) {
$fields['display_name'] = $name;
} elseif (!empty($extType)) {
$fields['display_name'] = ucfirst($extType) . ' - ' . $name;
}
}
// Target Joomla version
if (preg_match('/<targetplatform\s[^>]*version="([^"]+)"/', $xml, $m)) {
$fields['target_version'] = trim($m[1]);
} else {
// Default for Joomla 5/6
$fields['target_version'] = '(5|6)\..*';
}
// PHP minimum
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
$fields['php_minimum'] = trim($m[1]);
}
// License
if (preg_match('/<license>([^<]+)<\/license>/', $xml, $m)) {
$fields['license_spdx'] = $this->normalizeLicense(trim($m[1]));
}
}
private function findJoomlaManifest(string $root): ?string
{
// Priority: pkg_*.xml (package manifest)
$pkgXmls = array_merge(
SourceResolver::globSource($root, 'pkg_*.xml'),
glob("{$root}/pkg_*.xml") ?: []
);
if (!empty($pkgXmls)) {
return $pkgXmls[0];
}
// Any extension XML in source dir
foreach (SourceResolver::globSource($root, '*.xml') as $file) {
$content = file_get_contents($file);
if (strpos($content, '<extension') !== false) {
return $file;
}
}
// Root level
foreach (glob("{$root}/*.xml") ?: [] as $file) {
$content = file_get_contents($file);
if (strpos($content, '<extension') !== false) {
return $file;
}
}
return null;
}
// ── Dolibarr ────────────────────────────────────────────────────
private function detectDolibarr(string $root, string $repoName, array &$fields): void
{
$fields['language'] = 'PHP';
$fields['package_type'] = 'dolibarr-module';
$modFile = $this->findDolibarrModule($root);
if ($modFile === null) {
return;
}
$content = file_get_contents($modFile);
// Element name from class file
$modBasename = basename($modFile, '.class.php');
$fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename));
// Name
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
$fields['name'] = $m[1];
}
// Version
if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
$fields['version'] = $m[1];
}
// Description
if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
$desc = $m[1];
if (strpos($desc, '$') === false) {
$fields['description'] = $desc;
}
}
// License
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
$fields['license_spdx'] = $m[1];
}
}
private function findDolibarrModule(string $root): ?string
{
$candidates = array_merge(
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
glob("{$root}/core/modules/mod*.class.php") ?: []
);
foreach ($candidates as $file) {
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
return $file;
}
}
return null;
}
// ── Go ──────────────────────────────────────────────────────────
private function detectGo(string $root, string $repoName, array &$fields): void
{
$fields['language'] = 'Go';
$fields['package_type'] = 'application';
$fields['entry_point'] = './';
$goMod = "{$root}/go.mod";
if (!file_exists($goMod)) {
return;
}
$content = file_get_contents($goMod);
// Module path → name
if (preg_match('/^module\s+(\S+)/m', $content, $m)) {
$modulePath = $m[1];
$parts = explode('/', $modulePath);
$fields['name'] = end($parts);
}
// Go version
if (preg_match('/^go\s+(\S+)/m', $content, $m)) {
// This is Go language version, not the project version
// Project version comes from git tags or source files
}
// License
$fields['license_spdx'] = $this->detectLicense($root);
}
// ── Node / MCP ──────────────────────────────────────────────────
private function detectNode(string $root, string $repoName, array &$fields): void
{
$pkgFile = "{$root}/package.json";
if (!file_exists($pkgFile)) {
return;
}
$pkg = json_decode(file_get_contents($pkgFile), true) ?? [];
$fields['name'] = $pkg['name'] ?? '';
// Strip npm scope
if (strpos($fields['name'], '/') !== false) {
$fields['name'] = explode('/', $fields['name'])[1];
}
$fields['version'] = $pkg['version'] ?? '';
$fields['description'] = $pkg['description'] ?? '';
$fields['license_spdx'] = $pkg['license'] ?? '';
// Language detection
if (file_exists("{$root}/tsconfig.json")) {
$fields['language'] = 'TypeScript';
} else {
$fields['language'] = 'JavaScript';
}
// Package type
$deps = array_merge(
array_keys($pkg['dependencies'] ?? []),
array_keys($pkg['devDependencies'] ?? [])
);
$isMcp = false;
foreach ($deps as $dep) {
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
$isMcp = true;
break;
}
}
$fields['package_type'] = $isMcp ? 'mcp-server' : 'application';
// Entry point
if (file_exists("{$root}/dist")) {
$fields['entry_point'] = 'dist/';
} elseif (file_exists("{$root}/src")) {
$fields['entry_point'] = 'src/';
} else {
$fields['entry_point'] = './';
}
}
// ── Generic ─────────────────────────────────────────────────────
private function detectGeneric(string $root, string $repoName, array &$fields): void
{
$fields['package_type'] = 'generic';
// Try to detect language from file extensions
$fields['language'] = $this->detectLanguageFromFiles($root);
$fields['license_spdx'] = $this->detectLicense($root);
}
// =====================================================================
// Shared detection helpers
// =====================================================================
private function detectEntryPoint(string $root): string
{
$abs = SourceResolver::resolveAbsolute($root);
if ($abs !== null) {
return basename($abs) . '/';
}
if (is_dir("{$root}/dist")) return 'dist/';
if (is_dir("{$root}/src")) return 'src/';
return './';
}
private function detectLicense(string $root): string
{
// Check LICENSE file
foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) {
$file = "{$root}/{$name}";
if (!file_exists($file)) continue;
$content = file_get_contents($file);
// SPDX header
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
return $m[1];
}
// Common license patterns
if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) {
if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later';
if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later';
}
if (strpos($content, 'MIT License') !== false) return 'MIT';
if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0';
}
return '';
}
private function detectLanguageFromFiles(string $root): string
{
$counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0];
$extensions = [
'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript',
'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell',
];
// Quick scan: only check top two levels
foreach (glob("{$root}/*") ?: [] as $item) {
$ext = pathinfo($item, PATHINFO_EXTENSION);
if (isset($extensions[$ext])) {
$counts[$extensions[$ext]]++;
}
if (is_dir($item) && basename($item)[0] !== '.') {
foreach (glob("{$item}/*") ?: [] as $subItem) {
$ext = pathinfo($subItem, PATHINFO_EXTENSION);
if (isset($extensions[$ext])) {
$counts[$extensions[$ext]]++;
}
}
}
}
arsort($counts);
$top = key($counts);
return $counts[$top] > 0 ? $top : '';
}
private function normalizeLicense(string $license): string
{
$lower = strtolower($license);
$isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false;
if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later';
if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later';
if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT';
if (strpos($lower, 'apache') !== false) return 'Apache-2.0';
return $license;
}
private function detectRepoName(string $root): string
{
$gitConfig = "{$root}/.git/config";
if (!file_exists($gitConfig)) {
return basename($root);
}
$content = file_get_contents($gitConfig);
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
return $m[1];
}
return basename($root);
}
// =====================================================================
// API interaction
// =====================================================================
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
{
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
$ctx = stream_context_create([
'http' => [
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
'timeout' => 10,
],
]);
$body = @file_get_contents($url, false, $ctx);
if ($body === false) return null;
return json_decode($body, true);
}
private function computeDiff(array $current, array $detected): array
{
// Map detected keys to API keys (underscores match)
$changes = [];
foreach ($detected as $key => $value) {
$apiKey = $key;
$currentVal = $current[$apiKey] ?? '';
// Only flag as changed if detected value is non-empty and differs
if ($value !== '' && $value !== $currentVal) {
// Don't overwrite a non-empty API value with a detected value
// unless the API value is actually empty
if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) {
$changes[$key] = [
'current' => $currentVal,
'detected' => $value,
];
}
}
}
return $changes;
}
private function shouldOverride(string $field, string $current, string $detected): bool
{
// Version: detected from source is authoritative
if ($field === 'version') return true;
// These fields: source files are authoritative
if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) {
return true;
}
// For other fields, only fill empty — don't overwrite manual edits
return false;
}
private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool
{
$merged = array_merge($current, $update);
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
$payload = json_encode($merged);
$ctx = stream_context_create([
'http' => [
'method' => 'PUT',
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
'content' => $payload,
'timeout' => 10,
],
]);
$body = @file_get_contents($url, false, $ctx);
return $body !== false;
}
}
$app = new ManifestDetectCli();
exit($app->execute());
+7 -6
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/manifest_element.php * PATH: /cli/manifest_element.php
* BRIEF: Extract element name, type, type prefix, and ZIP name from manifest * BRIEF: Extract element name, type, type prefix, and ZIP name from manifest
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
class ManifestElementCli extends CliFramework class ManifestElementCli extends CliFramework
{ {
@@ -48,7 +48,7 @@ class ManifestElementCli extends CliFramework
} }
} }
$extManifest = null; $extManifest = null;
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []); $manifestFiles = array_merge(glob("{$root}/src/pkg_*.xml") ?: [], glob("{$root}/src/*.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,7 +58,8 @@ class ManifestElementCli extends CliFramework
} }
$modFile = null; $modFile = null;
$modFiles = array_merge( $modFiles = array_merge(
SourceResolver::globSource($root, 'core/modules/mod*.class.php'), glob("{$root}/src/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) {
-564
View File
@@ -1,564 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_integrity.php
* VERSION: 09.38.00
* BRIEF: Cross-check manifest API fields against repo contents across the org
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework;
class ManifestIntegrityCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Cross-check manifest fields against repo contents across the org');
$this->addArgument('--path', 'Single repo path (local mode)', '');
$this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting');
$this->addArgument('--repo', 'Single repo name (remote mode)', '');
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
$this->addArgument('--fix', 'Push fixes for detected drift', false);
$this->addArgument('--json', 'Output as JSON', false);
$this->addArgument('--quiet', 'Only show repos with issues', false);
}
protected function run(): int
{
$path = $this->getArgument('--path');
$org = $this->getArgument('--org');
$repoName = $this->getArgument('--repo');
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
$apiBase = rtrim($this->getArgument('--api-base'), '/');
$fixMode = (bool) $this->getArgument('--fix');
$jsonMode = (bool) $this->getArgument('--json');
$quiet = (bool) $this->getArgument('--quiet');
if ($token === '') {
$this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)');
return 1;
}
// ── Mode selection ──────────────────────────────────────────
if ($path !== '') {
// Local mode: detect from source + compare to API
return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
}
if ($repoName !== '') {
// Single remote repo
return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
}
// Bulk mode: all repos in org
return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet);
}
// =====================================================================
// Local mode — detect from source, compare to API
// =====================================================================
private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
{
$root = realpath($path) ?: $path;
if (!is_dir($root)) {
$this->log('ERROR', "Path does not exist: {$path}");
return 1;
}
if ($repoName === '') {
$repoName = $this->detectRepoName($root);
}
// Run manifest_detect logic
$detected = $this->runDetect($root, $repoName);
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
if ($current === null) {
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
return 1;
}
$issues = $this->validate($current, $detected, $repoName);
if ($json) {
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
} else {
$this->printIssues($repoName, $issues);
}
if ($fix && !empty($issues)) {
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
}
return empty($issues) ? 0 : 1;
}
// =====================================================================
// Remote single repo mode — fetch source files via API
// =====================================================================
private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
{
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
if ($current === null) {
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
return 1;
}
$issues = $this->validateManifestOnly($current, $repoName);
if ($json) {
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
} else {
$this->printIssues($repoName, $issues);
}
if ($fix && !empty($issues)) {
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
}
return empty($issues) ? 0 : 1;
}
// =====================================================================
// Bulk org mode — check all repos
// =====================================================================
private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int
{
$repos = $this->fetchOrgRepos($apiBase, $org, $token);
if ($repos === null) {
$this->log('ERROR', "Failed to fetch repos for org {$org}");
return 1;
}
$this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)");
$allResults = [];
$totalIssues = 0;
$reposWithIssues = 0;
foreach ($repos as $repo) {
$name = $repo['name'];
$manifest = $this->fetchManifest($apiBase, $org, $name, $token);
if ($manifest === null) {
if (!$quiet) {
$this->log('WARN', "{$name}: no manifest");
}
continue;
}
$issues = $this->validateManifestOnly($manifest, $name);
if (!empty($issues)) {
$reposWithIssues++;
$totalIssues += count($issues);
if ($json) {
$allResults[] = ['repo' => $name, 'issues' => $issues];
} else {
$this->printIssues($name, $issues);
}
if ($fix) {
$this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues);
}
} elseif (!$quiet && !$json) {
$this->log('OK', "{$name}: clean");
}
}
if ($json) {
echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
} else {
echo "\n";
$level = $reposWithIssues > 0 ? 'WARN' : 'OK';
$this->log($level, sprintf(
'Summary: %d repos checked, %d with issues (%d total issues)',
count($repos),
$reposWithIssues,
$totalIssues
));
}
return $reposWithIssues > 0 ? 1 : 0;
}
// =====================================================================
// Validation rules
// =====================================================================
/**
* Full validation: compare API manifest against locally-detected fields.
*/
private function validate(array $current, array $detected, string $repoName): array
{
$issues = [];
// Required fields that should never be empty
$required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
foreach ($required as $field) {
if (empty($current[$field])) {
$fix = $detected[$field] ?? null;
$issues[] = [
'field' => $field,
'severity' => 'error',
'message' => 'Missing required field',
'current' => '',
'fix' => $fix,
];
}
}
// Drift detection: detected value differs from API
foreach ($detected as $field => $detectedValue) {
$currentValue = $current[$field] ?? '';
if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) {
// Version drift is expected on dev branches (suffix)
if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) {
continue; // e.g., detected "02.34.50-dev" vs API "02.34.50"
}
if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) {
continue;
}
$issues[] = [
'field' => $field,
'severity' => 'warn',
'message' => 'Drift: source differs from manifest',
'current' => $currentValue,
'fix' => $detectedValue,
];
}
}
// Platform-specific structure validation
$platform = $current['platform'] ?? '';
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName));
return $issues;
}
/**
* API-only validation: check manifest fields for completeness and consistency
* without access to source files.
*/
private function validateManifestOnly(array $manifest, string $repoName): array
{
$issues = [];
// Required fields
$required = ['platform', 'name', 'version', 'language'];
foreach ($required as $field) {
if (empty($manifest[$field])) {
$issues[] = [
'field' => $field,
'severity' => 'error',
'message' => 'Missing required field',
'current' => '',
'fix' => null,
];
}
}
// Recommended fields
$recommended = ['package_type', 'entry_point', 'license_spdx', 'description'];
foreach ($recommended as $field) {
if (empty($manifest[$field])) {
$issues[] = [
'field' => $field,
'severity' => 'info',
'message' => 'Recommended field is empty',
'current' => '',
'fix' => null,
];
}
}
// Platform-specific checks
$platform = $manifest['platform'] ?? '';
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName));
return $issues;
}
/**
* Platform-specific validation rules.
*/
private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array
{
$issues = [];
switch ($platform) {
case 'joomla':
case 'waas-component':
// Joomla repos must have element_name
if (empty($manifest['element_name'])) {
$issues[] = [
'field' => 'element_name',
'severity' => 'error',
'message' => 'Joomla repos require element_name',
'current' => '',
'fix' => null,
];
}
// Language should be PHP
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
$issues[] = [
'field' => 'language',
'severity' => 'warn',
'message' => 'Joomla repos should have language=PHP',
'current' => $manifest['language'],
'fix' => 'PHP',
];
}
break;
case 'dolibarr':
case 'crm-module':
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
$issues[] = [
'field' => 'language',
'severity' => 'warn',
'message' => 'Dolibarr repos should have language=PHP',
'current' => $manifest['language'],
'fix' => 'PHP',
];
}
break;
case 'go':
if (!empty($manifest['language']) && $manifest['language'] !== 'Go') {
$issues[] = [
'field' => 'language',
'severity' => 'warn',
'message' => 'Go repos should have language=Go',
'current' => $manifest['language'],
'fix' => 'Go',
];
}
break;
case 'mcp':
if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) {
$issues[] = [
'field' => 'language',
'severity' => 'warn',
'message' => 'MCP repos should have language=TypeScript or JavaScript',
'current' => $manifest['language'],
'fix' => null,
];
}
break;
}
// Version format check: should be XX.YY.ZZ
$version = $manifest['version'] ?? '';
if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) {
// Allow semver for node/go repos
if (!in_array($platform, ['mcp', 'node', 'go'], true)) {
$issues[] = [
'field' => 'version',
'severity' => 'info',
'message' => 'Version does not match XX.YY.ZZ format',
'current' => $version,
'fix' => null,
];
}
}
return $issues;
}
// =====================================================================
// Output
// =====================================================================
private function printIssues(string $repoName, array $issues): void
{
if (empty($issues)) {
return;
}
$errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error'));
$warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn'));
$infos = count($issues) - $errors - $warns;
echo "\n";
$summary = [];
if ($errors > 0) $summary[] = "{$errors} error(s)";
if ($warns > 0) $summary[] = "{$warns} warning(s)";
if ($infos > 0) $summary[] = "{$infos} info";
$this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName}" . implode(', ', $summary));
foreach ($issues as $issue) {
$icon = match ($issue['severity']) {
'error' => 'ERROR',
'warn' => 'WARN',
default => 'INFO',
};
$msg = sprintf(' %-18s %s', $issue['field'], $issue['message']);
if ($issue['current'] !== '') {
$msg .= " (current: {$issue['current']})";
}
if ($issue['fix'] !== null) {
$msg .= " → fix: {$issue['fix']}";
}
$this->log($icon, $msg);
}
}
// =====================================================================
// Fix application
// =====================================================================
private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int
{
$fixes = [];
foreach ($issues as $issue) {
if ($issue['fix'] !== null && $issue['fix'] !== '') {
$fixes[$issue['field']] = $issue['fix'];
}
}
if (empty($fixes)) {
$this->log('INFO', "{$repo}: no auto-fixable issues");
return 0;
}
$merged = array_merge($current, $fixes);
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
$payload = json_encode($merged);
$ctx = stream_context_create([
'http' => [
'method' => 'PUT',
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
'content' => $payload,
'timeout' => 10,
],
]);
$body = @file_get_contents($url, false, $ctx);
if ($body === false) {
$this->log('ERROR', "{$repo}: failed to push fixes");
return 1;
}
$this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes)));
return 0;
}
// =====================================================================
// API helpers
// =====================================================================
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
{
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
$ctx = stream_context_create([
'http' => [
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
'timeout' => 10,
],
]);
$body = @file_get_contents($url, false, $ctx);
if ($body === false) return null;
$data = json_decode($body, true);
return is_array($data) ? $data : null;
}
private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array
{
$allRepos = [];
$page = 1;
$limit = 50;
while (true) {
$url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}";
$ctx = stream_context_create([
'http' => [
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
'timeout' => 15,
],
]);
$body = @file_get_contents($url, false, $ctx);
if ($body === false) return null;
$repos = json_decode($body, true);
if (!is_array($repos) || empty($repos)) break;
$allRepos = array_merge($allRepos, $repos);
if (count($repos) < $limit) break;
$page++;
}
// Filter out archived and empty repos
return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false));
}
// =====================================================================
// Detection (delegates to manifest_detect logic)
// =====================================================================
private function runDetect(string $root, string $repoName): array
{
$script = __DIR__ . '/manifest_detect.php';
$redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null';
$cmd = sprintf(
'php %s --path %s --repo %s --json --quiet %s',
escapeshellarg($script),
escapeshellarg($root),
escapeshellarg($repoName),
$redirect
);
$output = shell_exec($cmd) ?? '';
// Extract JSON object from output (skip banner/log lines)
if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) {
$data = json_decode($m[0], true);
if (is_array($data)) {
return $data;
}
}
return [];
}
private function detectRepoName(string $root): string
{
$gitConfig = "{$root}/.git/config";
if (!file_exists($gitConfig)) {
return basename($root);
}
$content = file_get_contents($gitConfig);
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
return $m[1];
}
return basename($root);
}
}
$app = new ManifestIntegrityCli();
exit($app->execute());
-280
View File
@@ -1,280 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_licensing.php
* VERSION: 09.38.00
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{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());
+94 -398
View File
@@ -6,431 +6,126 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/manifest_read.php * PATH: /cli/manifest_read.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest * BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption
*/ */
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ManifestReadCli extends CliFramework class ManifestReadCli extends CliFramework
{ {
/** Joomla extension XML element names searched in root and source/ dirs. */
private const JOOMLA_XML_ROOTS = ['extension', 'install'];
protected function configure(): void protected function configure(): void
{ {
$this->setDescription('Read repo metadata from Gitea API with auto-detection fallback'); $this->setDescription('Parse manifest.xml and output requested field(s) for CI consumption');
$this->addArgument('--path', 'Repository root path', '.'); $this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--field', 'Single field name to output', ''); $this->addArgument('--field', 'Single field name to output', '');
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false); $this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
$this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false); $this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false);
$this->addArgument('--json', 'Output all fields as JSON', false); $this->addArgument('--json', 'Output all fields as JSON', false);
} }
protected function run(): int protected function run(): int
{ {
$path = $this->getArgument('--path'); $path = $this->getArgument('--path');
$field = $this->getArgument('--field'); $field = $this->getArgument('--field');
$showAll = $this->getArgument('--all'); $showAll = $this->getArgument('--all');
$ghOut = $this->getArgument('--github-output'); $ghOutput = $this->getArgument('--github-output');
$jsonMode = $this->getArgument('--json'); $jsonMode = $this->getArgument('--json');
$mode = match (true) { // Determine mode
(bool) $ghOut => 'github-output', if ($ghOutput) {
(bool) $showAll => 'all', $mode = 'github-output';
(bool) $jsonMode => 'json', } elseif ($showAll) {
default => 'field', $mode = 'all';
}; } elseif ($jsonMode) {
$mode = 'json';
} else {
$mode = 'field';
}
// -- Locate manifest --
$root = realpath($path) ?: $path; $root = realpath($path) ?: $path;
$manifestFile = null;
// ── 1. Resolve org/repo ────────────────────────────────────────── // Priority: manifest.xml (current standard)
[$org, $repo] = $this->resolveOrgRepo($root); $candidates = [
"{$root}/.mokogitea/manifest.xml",
// ── 2. Primary: Gitea manifest API ─────────────────────────────── "{$root}/.mokogitea/.manifest.xml", // legacy (dot-prefixed)
$fields = null; "{$root}/.mokogitea/.moko-platform", // legacy v4
if ($org !== '' && $repo !== '') {
$fields = $this->fetchFromApi($org, $repo);
}
// ── 3. Fallback: auto-detect from source tree ────────────────────
if ($fields === null) {
$this->log('INFO', 'API unavailable — falling back to source-tree detection');
$fields = $this->autoDetect($root, $repo);
}
if (empty($fields)) {
$this->log('ERROR', "Could not resolve metadata for {$root}");
return 1;
}
// Provide backward-compatible aliases (hyphenated → underscore)
$fields = $this->addAliases($fields);
// Strip empty values
$fields = array_filter($fields, fn($v) => $v !== '' && $v !== null);
// ── 4. Output ────────────────────────────────────────────────────
return $this->outputFields($fields, $mode, $field);
}
// ── Gitea manifest API ───────────────────────────────────────────────
private function fetchFromApi(string $org, string $repo): ?array
{
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
$baseUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
$baseUrl = rtrim($baseUrl, '/');
if ($token === '') {
return null;
}
$url = "{$baseUrl}/api/v1/repos/{$org}/{$repo}/manifest";
$ctx = stream_context_create([
'http' => [
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
'timeout' => 10,
'ignore_errors' => true,
],
]);
$body = @file_get_contents($url, false, $ctx);
if ($body === false) {
return null;
}
// Check HTTP status from response headers
$status = 0;
if (isset($http_response_header[0])) {
preg_match('/\d{3}/', $http_response_header[0], $m);
$status = (int) ($m[0] ?? 0);
}
if ($status < 200 || $status >= 300) {
return null;
}
$data = json_decode($body, true);
if (!is_array($data) || empty($data)) {
return null;
}
$this->log('INFO', "Loaded metadata from Gitea manifest API ({$org}/{$repo})");
return $data;
}
// ── Auto-detection fallback ──────────────────────────────────────────
private function autoDetect(string $root, string $repoName): array
{
$fields = [
'name' => $repoName ?: basename($root),
'org' => 'MokoConsulting',
]; ];
// Resolve source directory (source/ or src/) foreach ($candidates as $candidate) {
$srcDir = null; if (file_exists($candidate)) {
foreach (['source', 'src'] as $candidate) { $manifestFile = $candidate;
if (is_dir("{$root}/{$candidate}")) {
$srcDir = $candidate;
break; break;
} }
} }
// ── Try Joomla detection ───────────────────────────────────── if ($manifestFile === null) {
$joomlaResult = $this->detectJoomla($root, $srcDir); $this->log('ERROR', "No manifest found in {$root}");
if ($joomlaResult !== null) { return 1;
$fields = array_merge($fields, $joomlaResult);
$this->log('INFO', "Auto-detected platform: joomla ({$fields['extension_type']}{$fields['element_name']})");
return $fields;
} }
// ── Try Dolibarr detection ─────────────────────────────────── // -- Parse XML --
$dolibarrResult = $this->detectDolibarr($root); $xml = @simplexml_load_file($manifestFile);
if ($dolibarrResult !== null) {
$fields = array_merge($fields, $dolibarrResult);
$this->log('INFO', "Auto-detected platform: dolibarr");
return $fields;
}
// ── Generic fallback ───────────────────────────────────────── if ($xml === false) {
$fields['platform'] = $this->detectGenericPlatform($root); // Fallback: try YAML format (.mokostandards legacy)
$fields['element_name'] = strtolower($fields['name']); $content = file_get_contents($manifestFile);
$fields['extension_type'] = 'application'; $fields = [];
$fields['language'] = $this->detectLanguage($root); if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
if ($srcDir !== null) { $fields['platform'] = trim($m[1], " \t\n\r\"'");
$fields['entry_point'] = "{$srcDir}/";
}
$this->log('INFO', "Auto-detected platform: {$fields['platform']}");
return $fields;
}
/**
* Detect Joomla platform by scanning for extension XML manifests.
*
* Searches root and source/ dirs for XML files containing <extension type="...">.
* Extracts element name from the filename (pkg_*, com_*, mod_*, plg_*, tpl_*) or
* from the <element> tag inside the manifest.
*/
private function detectJoomla(string $root, ?string $srcDir): ?array
{
$searchDirs = [$root];
if ($srcDir !== null) {
$searchDirs[] = "{$root}/{$srcDir}";
}
foreach ($searchDirs as $dir) {
$xmlFiles = glob("{$dir}/*.xml") ?: [];
foreach ($xmlFiles as $xmlFile) {
$content = @file_get_contents($xmlFile);
if ($content === false) {
continue;
}
// Match <extension type="component|module|plugin|package|template|file|library">
if (!preg_match('/<extension\s+[^>]*type="([^"]+)"/', $content, $typeMatch)) {
// Also try legacy <install type="...">
if (!preg_match('/<install\s+[^>]*type="([^"]+)"/', $content, $typeMatch)) {
continue;
}
}
$extType = strtolower($typeMatch[1]);
$basename = pathinfo($xmlFile, PATHINFO_FILENAME);
// Try to extract element name from XML <element> tag
$xml = @simplexml_load_string($content);
$element = '';
if ($xml !== false) {
// Package manifests have <files><file ...>element</file></files>
// Component/module manifests have <element> or use filename
$element = (string) ($xml->element ?? '');
if ($element === '') {
$element = strtolower($basename);
}
} else {
$element = strtolower($basename);
}
// Derive display name
$displayName = (string) ($xml->name ?? ucfirst(str_replace('_', ' ', $basename)));
return [
'platform' => 'joomla',
'extension_type' => $extType,
'element_name' => $element,
'display_name' => $displayName,
'language' => 'PHP',
'entry_point' => ($srcDir ?? '.') . '/',
];
} }
if (preg_match('/^standards_version:\s*(.+)/m', $content, $m)) {
// Also check for pkg_*.xml pattern specifically $fields['standards-version'] = trim($m[1], " \t\n\r\"'");
$pkgFiles = glob("{$dir}/pkg_*.xml") ?: [];
if (!empty($pkgFiles)) {
$basename = pathinfo($pkgFiles[0], PATHINFO_FILENAME);
return [
'platform' => 'joomla',
'extension_type' => 'package',
'element_name' => strtolower($basename),
'display_name' => ucfirst(str_replace('_', ' ', $basename)),
'language' => 'PHP',
'entry_point' => ($srcDir ?? '.') . '/',
];
} }
} if (preg_match('/^governed_repo:\s*(.+)/m', $content, $m)) {
$fields['name'] = trim($m[1], " \t\n\r\"'");
// Check for com_*/manifest.xml pattern (component subdirectory)
$comDirs = glob("{$root}/com_*", GLOB_ONLYDIR) ?: [];
foreach ($comDirs as $comDir) {
$comManifest = glob("{$comDir}/*.xml") ?: [];
foreach ($comManifest as $xmlFile) {
$content = @file_get_contents($xmlFile);
if ($content && preg_match('/<extension\s+[^>]*type="component"/', $content)) {
return [
'platform' => 'joomla',
'extension_type' => 'component',
'element_name' => strtolower(basename($comDir)),
'display_name' => ucfirst(str_replace('com_', '', basename($comDir))),
'language' => 'PHP',
'entry_point' => ($srcDir ?? '.') . '/',
];
}
} }
} } else {
// Register namespace for XPath (optional, simple path works without)
return null; $fields = [
} 'name' => (string)($xml->identity->name ?? ''),
'display-name' => (string)($xml->identity->{"display-name"} ?? ''),
/** 'org' => (string)($xml->identity->org ?? ''),
* Detect Dolibarr platform by scanning for module descriptor files. 'description' => (string)($xml->identity->description ?? ''),
*/ 'license' => (string)($xml->identity->license ?? ''),
private function detectDolibarr(string $root): ?array 'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''),
{ 'platform' => (string)($xml->governance->platform ?? ''),
// Look for mod*.class.php containing DolibarrModules 'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''),
$searchPaths = [ 'standards-source' => (string)($xml->governance->{"standards-source"} ?? ''),
"{$root}/core/modules/mod*.class.php", 'language' => (string)($xml->build->language ?? ''),
"{$root}/*/core/modules/mod*.class.php", 'package-type' => (string)($xml->build->{"package-type"} ?? ''),
]; 'entry-point' => (string)($xml->build->{"entry-point"} ?? ''),
'version' => (string)($xml->identity->version ?? ''),
foreach ($searchPaths as $pattern) { 'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''),
$files = glob($pattern) ?: []; 'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''),
foreach ($files as $file) { 'excludes' => (string)($xml->deploy->excludes ?? ''),
$content = @file_get_contents($file); 'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''),
if ($content && str_contains($content, 'DolibarrModules')) { 'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''),
$modName = pathinfo($file, PATHINFO_FILENAME); 'manifest-file' => $manifestFile,
// modMyModule.class → mymodule
$element = strtolower(preg_replace('/^mod/', '', str_replace('.class', '', $modName)));
return [
'platform' => 'dolibarr',
'extension_type' => 'module',
'element_name' => $element,
'display_name' => ucfirst($element),
'language' => 'PHP',
'entry_point' => './',
];
}
}
}
// Secondary: check for update.txt (Dolibarr marker)
if (file_exists("{$root}/update.txt")) {
return [
'platform' => 'dolibarr',
'extension_type' => 'module',
'element_name' => strtolower(basename($root)),
'display_name' => basename($root),
'language' => 'PHP',
'entry_point' => './',
]; ];
} }
return null; // Strip empty values for cleaner output
} $fields = array_filter($fields, fn($v) => $v !== '');
/** // -- Output --
* Detect generic platform type (php, nodejs, python, etc.) from project files.
*/
private function detectGenericPlatform(string $root): string
{
if (file_exists("{$root}/composer.json")) {
return 'php';
}
if (file_exists("{$root}/package.json")) {
return 'nodejs';
}
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
return 'python';
}
if (file_exists("{$root}/go.mod")) {
return 'go';
}
if (file_exists("{$root}/Cargo.toml")) {
return 'rust';
}
return 'generic';
}
/**
* Detect primary language from project files.
*/
private function detectLanguage(string $root): string
{
if (file_exists("{$root}/composer.json")) {
return 'PHP';
}
if (file_exists("{$root}/tsconfig.json")) {
return 'TypeScript';
}
if (file_exists("{$root}/package.json")) {
return 'JavaScript';
}
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
return 'Python';
}
return '';
}
// ── Org/repo resolution ──────────────────────────────────────────────
/**
* Resolve org and repo name from environment or git remote.
*
* @return array{0: string, 1: string} [org, repo]
*/
private function resolveOrgRepo(string $root): array
{
// 1. GITHUB_REPOSITORY env (set in Gitea Actions / GitHub Actions)
$envRepo = getenv('GITHUB_REPOSITORY') ?: '';
if ($envRepo !== '' && str_contains($envRepo, '/')) {
return explode('/', $envRepo, 2);
}
// 2. Parse git remote origin URL
$remoteUrl = trim((string) shell_exec(
'git -C ' . escapeshellarg($root) . ' remote get-url origin 2>/dev/null'
));
if ($remoteUrl !== '') {
// SSH: git@host:Org/Repo.git or HTTPS: https://host/Org/Repo.git
if (preg_match('#[/:]([^/]+)/([^/]+?)(?:\.git)?$#', $remoteUrl, $m)) {
return [$m[1], $m[2]];
}
}
return ['', basename($root)];
}
// ── Backward-compatible aliases ──────────────────────────────────────
/**
* Add hyphenated aliases for underscore fields (backward compat with old manifest.xml consumers).
* Also map old field names to new ones.
*/
private function addAliases(array $fields): array
{
// Map API field names → old manifest.xml hyphenated names
$aliases = [
'display_name' => 'display-name',
'license_spdx' => 'license-spdx',
'license_name' => 'license',
'standards_version' => 'standards-version',
'standards_source' => 'standards-source',
'extension_type' => 'package-type',
'entry_point' => 'entry-point',
'element_name' => 'name',
];
foreach ($aliases as $newKey => $oldKey) {
if (isset($fields[$newKey]) && !isset($fields[$oldKey])) {
$fields[$oldKey] = $fields[$newKey];
}
}
return $fields;
}
// ── Output ───────────────────────────────────────────────────────────
private function outputFields(array $fields, string $mode, string $field): int
{
switch ($mode) { switch ($mode) {
case 'field': case 'field':
if ($field === '') { if ($field === '') {
$this->log('ERROR', "Usage: manifest:read --path <dir> --field <name>"); $this->log('ERROR', "Usage: manifest_read.php --path <dir> --field <name>");
$this->log('ERROR', " manifest:read --path <dir> --all"); $this->log('ERROR', " manifest_read.php --path <dir> --all");
$this->log('ERROR', " manifest:read --path <dir> --json"); $this->log('ERROR', " manifest_read.php --path <dir> --json");
$this->log('ERROR', " manifest:read --path <dir> --github-output"); $this->log('ERROR', " manifest_read.php --path <dir> --github-output");
return 2; return 2;
} }
echo ($fields[$field] ?? '') . "\n"; echo ($fields[$field] ?? '') . "\n";
@@ -447,21 +142,22 @@ class ManifestReadCli extends CliFramework
break; break;
case 'github-output': case 'github-output':
$outputFile = getenv('GITHUB_OUTPUT') ?: getenv('GITEA_OUTPUT') ?: ''; $outputFile = getenv('GITHUB_OUTPUT');
$lines = []; if ($outputFile === false || $outputFile === '') {
foreach ($fields as $k => $v) { $this->log('ERROR', 'GITHUB_OUTPUT not set — printing to stdout instead');
$envKey = str_replace('-', '_', $k); foreach ($fields as $k => $v) {
$lines[$envKey] = "{$envKey}={$v}\n"; // Convert field-name to FIELD_NAME for env var style
} $envKey = str_replace('-', '_', $k);
// Deduplicate (aliases may collide after underscore conversion) echo "{$envKey}={$v}\n";
$output = implode('', $lines); }
if ($outputFile === '') {
$this->log('WARNING', 'GITHUB_OUTPUT not set — printing to stdout');
echo $output;
} else { } else {
file_put_contents($outputFile, $output, FILE_APPEND); $fh = fopen($outputFile, 'a');
$this->log('INFO', "Wrote " . count($lines) . " fields to GITHUB_OUTPUT"); foreach ($fields as $k => $v) {
$envKey = str_replace('-', '_', $k);
fwrite($fh, "{$envKey}={$v}\n");
}
fclose($fh);
$this->log('INFO', "Wrote " . count($fields) . " fields to GITHUB_OUTPUT");
} }
break; break;
} }
+12 -7
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/package_build.php * PATH: /cli/package_build.php
* BRIEF: Build ZIP and tar.gz install packages for Joomla/Dolibarr/generic projects * BRIEF: Build ZIP and tar.gz install packages for Joomla/Dolibarr/generic projects
* *
@@ -19,7 +19,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
class PackageBuildCli extends CliFramework class PackageBuildCli extends CliFramework
{ {
@@ -56,13 +56,18 @@ class PackageBuildCli extends CliFramework
} }
// -- Determine source directory ----------------------------------------------- // -- Determine source directory -----------------------------------------------
$sourceDir = SourceResolver::resolveAbsolute($root); $sourceDir = null;
foreach (['src', 'htdocs'] as $candidate) {
if (is_dir("{$root}/{$candidate}")) {
$sourceDir = "{$root}/{$candidate}";
break;
}
}
if ($sourceDir === null) { if ($sourceDir === null) {
$this->log('ERROR', "No source/ or src/ directory found in {$root}"); $this->log('ERROR', "No src/ or htdocs/ 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;
+20 -163
View File
@@ -6,32 +6,25 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/platform_detect.php * PATH: /cli/platform_detect.php
* VERSION: 09.38.00 * BRIEF: Detect platform from manifest.xml file — outputs platform string
* BRIEF: Auto-detect repository platform type and optionally update manifest
*/ */
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class PlatformDetectCli extends CliFramework class PlatformDetectCli extends CliFramework
{ {
protected function configure(): void protected function configure(): void
{ {
$this->setDescription('Auto-detect repository platform type and optionally update manifest'); $this->setDescription('Detect platform from manifest.xml file');
$this->addArgument('--path', 'Local repo path to scan (default: .)', '.'); $this->addArgument('--path', 'Repository root path', '.');
$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 protected function run(): int
@@ -39,161 +32,25 @@ class PlatformDetectCli extends CliFramework
$path = $this->getArgument('--path'); $path = $this->getArgument('--path');
$root = realpath($path) ?: $path; $root = realpath($path) ?: $path;
$token = $this->getArgument('--token'); // Check .mokogitea/manifest.xml first, fallback to root
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/'); $file = "{$root}/.mokogitea/manifest.xml";
$owner = $this->getArgument('--owner'); if (!file_exists($file)) {
$repo = $this->getArgument('--repo'); $file = "{$root}/.mokostandards";
$doUpdate = $this->isFlagSet('--update'); }
$githubOutput = $this->isFlagSet('--github-output'); if (!file_exists($file)) {
echo "unknown\n";
$platform = $this->detectPlatform($root); return 0;
$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.');
}
} }
// Update manifest via API if requested $content = file_get_contents($file);
if ($doUpdate) { if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
if ($token === '' || $owner === '' || $repo === '') { echo trim($m[1], " \t\n\r\"'") . "\n";
$this->log('ERROR', '--update requires --token, --owner, and --repo.'); } else {
return 1; echo "unknown\n";
}
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}/metadata",
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; 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 mokocli itself or org-config
$repoName = basename($root);
if (in_array($repoName, ['mokocli', '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(); $app = new PlatformDetectCli();
+6 -6
View File
@@ -6,24 +6,24 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release.php * PATH: /cli/release.php
* BRIEF: Automate the mokocli version branch release flow * BRIEF: Automate the moko-platform version branch release flow
*/ */
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseCli extends CliFramework class ReleaseCli extends CliFramework
{ {
protected function configure(): void protected function configure(): void
{ {
$this->setDescription('Automate the mokocli version branch release flow'); $this->setDescription('Automate the moko-platform version branch release flow');
$this->addArgument('--bump', 'Bump type: patch, minor, or major', ''); $this->addArgument('--bump', 'Bump type: patch, minor, or major', '');
} }
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_body_update.php * PATH: /cli/release_body_update.php
* BRIEF: Update Gitea release body with changelog extract and checksums * BRIEF: Update Gitea release body with changelog extract and checksums
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseBodyUpdateCli extends CliFramework class ReleaseBodyUpdateCli extends CliFramework
{ {
+9 -314
View File
@@ -6,336 +6,31 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_cascade.php * PATH: /cli/release_cascade.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Cascade release zip to all lower stability channels * BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent.
*/ */
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseCascadeCli extends CliFramework class ReleaseCascadeCli extends CliFramework
{ {
/** Channel hierarchy: highest stability first. */
private const CHANNELS = ['stable', 'release-candidate', 'beta', 'alpha', 'development'];
/** Map stability input names to canonical tag names. */
private const TAG_MAP = [
'stable' => 'stable',
'release-candidate' => 'release-candidate',
'rc' => 'release-candidate',
'beta' => 'beta',
'alpha' => 'alpha',
'development' => 'development',
'dev' => 'development',
];
protected function configure(): void protected function configure(): void
{ {
$this->setDescription('Cascade release zip to all lower stability channels'); $this->setDescription('DEPRECATED — cascade behavior removed');
$this->addArgument('--stability', 'Source stability channel (required)', '');
$this->addArgument('--token', 'Gitea API token (required)', '');
$this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', '');
} }
protected function run(): int protected function run(): int
{ {
$stability = strtolower($this->getArgument('--stability')); $this->log('INFO', 'No-op (cascade behavior removed — each stream is independent)');
$token = $this->getArgument('--token'); return 0;
$apiBase = rtrim($this->getArgument('--api-base'), '/');
if ($token === '') {
$envToken = getenv('MOKOGITEA_TOKEN');
if ($envToken === false || $envToken === '') {
$envToken = getenv('GITEA_TOKEN');
}
if ($envToken !== false && $envToken !== '') {
$token = $envToken;
}
}
if ($stability === '' || $token === '' || $apiBase === '') {
$this->log('ERROR', 'Usage: release_cascade.php --stability CHANNEL --token TOKEN --api-base URL');
return 1;
}
$sourceTag = self::TAG_MAP[$stability] ?? null;
if ($sourceTag === null) {
$this->log('ERROR', "Unknown stability: {$stability}");
return 1;
}
// Find lower channels to cascade to
$lowerChannels = $this->getLowerChannels($sourceTag);
if (count($lowerChannels) === 0) {
$this->log('INFO', "No lower channels for '{$stability}' — nothing to cascade.");
return 0;
}
$this->log('INFO', "Cascading from '{$sourceTag}' to: " . implode(', ', $lowerChannels));
if ($this->dryRun) {
$this->log('INFO', '[DRY RUN] No changes will be made.');
}
// 1. Get source release
$sourceRelease = $this->giteaApi("{$apiBase}/releases/tags/{$sourceTag}", $token);
if ($sourceRelease === null) {
$this->log('WARN', "No release found at tag '{$sourceTag}' — nothing to cascade.");
return 0;
}
$sourceVersion = $sourceRelease['name'] ?? $sourceTag;
$sourceBody = $sourceRelease['body'] ?? '';
$sourceAssets = $sourceRelease['assets'] ?? [];
// Find zip assets (exclude .sha256 sidecars)
$zipAssets = array_filter($sourceAssets, function (array $asset): bool {
$name = strtolower($asset['name'] ?? '');
return str_ends_with($name, '.zip') && !str_ends_with($name, '.sha256');
});
// Also grab sha256 sidecars
$sha256Assets = array_filter($sourceAssets, function (array $asset): bool {
return str_ends_with(strtolower($asset['name'] ?? ''), '.zip.sha256');
});
if (count($zipAssets) === 0) {
$this->log('WARN', "Source release '{$sourceTag}' has no zip assets — nothing to cascade.");
return 0;
}
$this->log('INFO', "Source: {$sourceVersion}" . count($zipAssets) . " zip(s)");
echo "\n";
// 2. Download source assets to temp files
$downloads = [];
foreach (array_merge($zipAssets, $sha256Assets) as $asset) {
$url = $asset['browser_download_url'] ?? '';
if ($url === '') {
continue;
}
$tmpFile = tempnam(sys_get_temp_dir(), 'cascade_');
if ($this->downloadFile($url, $token, $tmpFile)) {
$downloads[] = ['name' => $asset['name'], 'path' => $tmpFile];
$this->log('INFO', "Downloaded: {$asset['name']}");
} else {
$this->log('ERROR', "Failed to download: {$asset['name']}");
}
}
if (count($downloads) === 0) {
$this->log('ERROR', 'Could not download any source assets.');
return 1;
}
// 3. Cascade to each lower channel
$errors = 0;
foreach ($lowerChannels as $targetTag) {
echo "\n";
$result = $this->cascadeToChannel(
$apiBase, $token, $targetTag,
$sourceVersion, $sourceBody, $downloads
);
if (!$result) {
$errors++;
}
}
// 4. Cleanup temp files
foreach ($downloads as $dl) {
@unlink($dl['path']);
}
echo "\n";
$this->log('INFO', "Cascade complete. " . (count($lowerChannels) - $errors)
. "/" . count($lowerChannels) . " channels updated.");
return $errors > 0 ? 1 : 0;
}
/**
* Cascade assets to a single target channel.
*/
private function cascadeToChannel(
string $apiBase,
string $token,
string $targetTag,
string $sourceVersion,
string $sourceBody,
array $downloads
): bool {
$this->log('INFO', "{$targetTag}");
if ($this->dryRun) {
$this->log('INFO', " [DRY RUN] Would cascade to {$targetTag}");
return true;
}
// Find existing release at target tag
$existing = $this->giteaApi("{$apiBase}/releases/tags/{$targetTag}", $token);
if ($existing !== null && !empty($existing['id'])) {
$releaseId = (int) $existing['id'];
// Delete existing assets
$existingAssets = $existing['assets'] ?? [];
foreach ($existingAssets as $asset) {
$assetId = $asset['id'] ?? 0;
if ($assetId > 0) {
$this->giteaApi(
"{$apiBase}/releases/{$releaseId}/assets/{$assetId}",
$token, 'DELETE'
);
}
}
// Update release metadata
$updatePayload = json_encode([
'name' => $sourceVersion,
'body' => $sourceBody,
]);
$this->giteaApi(
"{$apiBase}/releases/{$releaseId}",
$token, 'PATCH', $updatePayload
);
$this->log('INFO', " Updated release metadata (id: {$releaseId})");
} else {
// Create new release at target tag
// Use the source release's target commitish so the tag points to the same commit
$createPayload = json_encode([
'tag_name' => $targetTag,
'target_commitish' => 'main',
'name' => $sourceVersion,
'body' => $sourceBody,
'prerelease' => ($targetTag !== 'stable'),
]);
$newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $createPayload);
if ($newRelease === null || empty($newRelease['id'])) {
$this->log('ERROR', " Failed to create release at tag '{$targetTag}'");
return false;
}
$releaseId = (int) $newRelease['id'];
$this->log('INFO', " Created release (id: {$releaseId})");
}
// Upload assets
foreach ($downloads as $dl) {
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . rawurlencode($dl['name']);
$success = $this->uploadAsset($uploadUrl, $token, $dl['path'], $dl['name']);
if ($success) {
$this->log('INFO', " Uploaded: {$dl['name']}");
} else {
$this->log('ERROR', " Failed to upload: {$dl['name']}");
}
}
return true;
}
/**
* Get all channels below the given source channel.
*/
private function getLowerChannels(string $sourceTag): array
{
$idx = array_search($sourceTag, self::CHANNELS, true);
if ($idx === false) {
return [];
}
return array_slice(self::CHANNELS, $idx + 1);
}
/**
* Download a file via HTTP.
*/
private function downloadFile(string $url, string $token, string $destPath): bool
{
$ch = curl_init($url);
if ($ch === false) {
return false;
}
$fp = fopen($destPath, 'wb');
if ($fp === false) {
return false;
}
curl_setopt_array($ch, [
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_FILE => $fp,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 120,
]);
curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
return $code >= 200 && $code < 300;
}
/**
* Upload a file as a release asset via multipart form.
*/
private function uploadAsset(string $url, string $token, string $filePath, string $fileName): bool
{
$ch = curl_init($url);
if ($ch === false) {
return false;
}
$cfile = new CURLFile($filePath, 'application/octet-stream', $fileName);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ['attachment' => $cfile],
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 120,
]);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $code >= 200 && $code < 300;
}
/**
* Make an HTTP request to the Gitea API.
*/
private function giteaApi(
string $url,
string $token,
string $method = 'GET',
?string $body = null
): ?array {
$ch = curl_init($url);
if ($ch === false) {
return null;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: token {$token}",
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => $method,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300 || empty($response) || !is_string($response)) {
return null;
}
$decoded = json_decode($response, true);
return is_array($decoded) ? $decoded : null;
} }
} }
+8 -7
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_create.php * PATH: /cli/release_create.php
* BRIEF: Create or overwrite a Gitea release with proper naming * BRIEF: Create or overwrite a Gitea release with proper naming
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
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(
SourceResolver::globSource($root, 'pkg_*.xml'), glob("{$root}/src/pkg_*.xml") ?: [],
SourceResolver::globSource($root, '*.xml'), glob("{$root}/src/*.xml") ?: [],
glob("{$root}/*.xml") ?: [] glob("{$root}/*.xml") ?: []
); );
foreach ($manifestFiles as $file) { foreach ($manifestFiles as $file) {
@@ -112,7 +112,8 @@ class ReleaseCreateCli extends CliFramework
// Find Dolibarr module file // Find Dolibarr module file
$modFile = null; $modFile = null;
$modFiles = array_merge( $modFiles = array_merge(
SourceResolver::globSource($root, 'core/modules/mod*.class.php'), glob("{$root}/src/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) {
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_manage.php * PATH: /cli/release_manage.php
* BRIEF: Create/update Gitea releases, upload assets, update release body * BRIEF: Create/update Gitea releases, upload assets, update release body
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseManageCli extends CliFramework class ReleaseManageCli extends CliFramework
{ {
+6 -6
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_mirror.php * PATH: /cli/release_mirror.php
* BRIEF: Mirror a Gitea release (with assets) to a GitHub repository * BRIEF: Mirror a Gitea release (with assets) to a GitHub repository
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseMirrorCli extends CliFramework class ReleaseMirrorCli extends CliFramework
{ {
@@ -201,7 +201,7 @@ class ReleaseMirrorCli extends CliFramework
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
"Authorization: token {$token}", "Authorization: token {$token}",
'Accept: application/vnd.github+json', 'Accept: application/vnd.github+json',
'User-Agent: mokocli', 'User-Agent: moko-platform',
'Content-Type: application/json', 'Content-Type: application/json',
], ],
CURLOPT_TIMEOUT => 30, CURLOPT_TIMEOUT => 30,
@@ -229,7 +229,7 @@ class ReleaseMirrorCli extends CliFramework
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
"Authorization: token {$token}", "Authorization: token {$token}",
'Accept: application/vnd.github+json', 'Accept: application/vnd.github+json',
'User-Agent: mokocli', 'User-Agent: moko-platform',
'Content-Type: application/octet-stream', 'Content-Type: application/octet-stream',
], ],
CURLOPT_POSTFIELDS => file_get_contents($filePath), CURLOPT_POSTFIELDS => file_get_contents($filePath),
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_notes.php * PATH: /cli/release_notes.php
* BRIEF: Extract release notes from CHANGELOG.md for a given version * BRIEF: Extract release notes from CHANGELOG.md for a given version
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseNotesCli extends CliFramework class ReleaseNotesCli extends CliFramework
{ {
+12 -52
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_package.php * PATH: /cli/release_package.php
* BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release * BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
class ReleasePackageCli extends CliFramework class ReleasePackageCli extends CliFramework
{ {
@@ -99,10 +99,9 @@ class ReleasePackageCli extends CliFramework
$extFolder = ''; $extFolder = '';
$typePrefix = ''; $typePrefix = '';
SourceResolver::warnIfLegacy($root);
$manifestFiles = array_merge( $manifestFiles = array_merge(
SourceResolver::globSource($root, 'pkg_*.xml'), glob("{$root}/src/pkg_*.xml") ?: [],
SourceResolver::globSource($root, '*.xml'), glob("{$root}/src/*.xml") ?: [],
glob("{$root}/*.xml") ?: [] glob("{$root}/*.xml") ?: []
); );
@@ -201,12 +200,14 @@ class ReleasePackageCli extends CliFramework
} }
} }
if ($sourceDir === null) { if ($sourceDir === null && is_dir("{$root}/src")) {
$sourceDir = SourceResolver::resolveAbsolute($root); $sourceDir = "{$root}/src";
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
$sourceDir = "{$root}/htdocs";
} }
if ($sourceDir === null) { if ($sourceDir === null) {
echo "No source/ or src/ directory found — skipping package build\n"; echo "No src/ or htdocs/ directory found — skipping package build\n";
return 0; return 0;
} }
@@ -224,58 +225,17 @@ class ReleasePackageCli extends CliFramework
return 1; return 1;
} }
// Only zip sub-extensions listed in the package manifest
$manifestedSubs = [];
$pkgManifestFiles = glob("{$sourceDir}/pkg_*.xml") ?: [];
foreach ($pkgManifestFiles as $pmf) {
$pmXml = @simplexml_load_file($pmf);
if ($pmXml && isset($pmXml->files)) {
foreach ($pmXml->files->file as $fileNode) {
$zipName = pathinfo((string) $fileNode, PATHINFO_FILENAME);
if (!empty($zipName)) {
$manifestedSubs[$zipName] = true;
}
}
}
}
$packageDirs = glob("{$sourceDir}/packages/*", GLOB_ONLYDIR) ?: []; $packageDirs = glob("{$sourceDir}/packages/*", GLOB_ONLYDIR) ?: [];
foreach ($packageDirs as $pkgDir) { foreach ($packageDirs as $pkgDir) {
$subName = basename($pkgDir); $subName = basename($pkgDir);
// Skip directories not listed in the package manifest
if (!empty($manifestedSubs) && !isset($manifestedSubs[$subName])) {
echo " Skipping {$subName} (not in package manifest)\n";
continue;
}
$subZipPath = "{$outputDir}/{$subName}.zip"; $subZipPath = "{$outputDir}/{$subName}.zip";
// If sub-package is a full repo checkout (e.g. git submodule),
// look for a source/ or src/ subdirectory containing a Joomla manifest XML
// and zip that instead of the repo root.
$subSourceDir = $pkgDir;
$subSrcAbs = SourceResolver::resolveAbsolute($pkgDir);
if ($subSrcAbs !== null) {
$srcManifests = array_merge(
glob("{$subSrcAbs}/*.xml") ?: [],
glob("{$subSrcAbs}/pkg_*.xml") ?: []
);
foreach ($srcManifests as $mf) {
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
$subSourceDir = $subSrcAbs;
$subSrcName = SourceResolver::resolve($pkgDir);
echo " Sub-package {$subName}: using {$subSrcName}/ entry-point\n";
break;
}
}
}
$subZip = new \ZipArchive(); $subZip = new \ZipArchive();
if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
$this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}"); $this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}");
continue; continue;
} }
$this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns); $this->addDirToZip($subZip, $pkgDir, '', $this->excludePatterns);
$subZip->close(); $subZip->close();
$zip->addFile($subZipPath, "packages/{$subName}.zip"); $zip->addFile($subZipPath, "packages/{$subName}.zip");
+6 -6
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_promote.php * PATH: /cli/release_promote.php
* BRIEF: Promote a Gitea release from one channel to another (rename release, tag, assets) * BRIEF: Promote a Gitea release from one channel to another (rename release, tag, assets)
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
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(
SourceResolver::globSource($root, 'pkg_*.xml'), glob("{$root}/src/pkg_*.xml") ?: [],
SourceResolver::globSource($root, '*.xml'), glob("{$root}/src/*.xml") ?: [],
glob("{$root}/*.xml") ?: [] glob("{$root}/*.xml") ?: []
); );
foreach ($manifestFiles as $xmlFile) { foreach ($manifestFiles as $xmlFile) {
+5 -5
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_publish.php * PATH: /cli/release_publish.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Publish a release and create copies for all lesser stability streams. * BRIEF: Publish a release and create copies for all lesser stability streams.
*/ */
@@ -18,7 +18,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleasePublishCli extends CliFramework class ReleasePublishCli extends CliFramework
{ {
+8 -11
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_validate.php * PATH: /cli/release_validate.php
* BRIEF: Pre-release validation -- version consistency, required files, manifest checks * BRIEF: Pre-release validation -- version consistency, required files, manifest checks
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
class ReleaseValidateCli extends CliFramework class ReleaseValidateCli extends CliFramework
{ {
@@ -66,10 +66,8 @@ class ReleaseValidateCli extends CliFramework
$platform = 'generic'; $platform = 'generic';
} }
} }
$hasSource = SourceResolver::resolveAbsolute($root) !== null; $hasSource = is_dir("{$root}/src") || is_dir("{$root}/htdocs");
SourceResolver::warnIfLegacy($root); $this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? 'src/ or htdocs/ found' : 'No src/ or htdocs/ directory');
$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 {
@@ -111,8 +109,7 @@ 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;
$srcAbs = SourceResolver::resolveAbsolute($root); foreach (["{$root}/src", $root] as $dir) {
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) {
@@ -159,7 +156,7 @@ class ReleaseValidateCli extends CliFramework
} }
} elseif ($platform === 'dolibarr') { } elseif ($platform === 'dolibarr') {
$modFile = null; $modFile = null;
foreach (SourceResolver::getCandidates() as $sd) { foreach (['src', 'htdocs'] 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 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_verify.php * PATH: /cli/release_verify.php
* BRIEF: Verify a built release artifact — version, SHA256, disallowed files * BRIEF: Verify a built release artifact — version, SHA256, disallowed files
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ReleaseVerifyCli extends CliFramework class ReleaseVerifyCli extends CliFramework
{ {
-429
View File
@@ -1,429 +0,0 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* FILE INFORMATION
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/repo_wizard.php
* BRIEF: Interactive configuration wizard for new repositories
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, Config, PlatformAdapterFactory};
/**
* Interactive repo setup wizard.
*
* Walks through platform selection, generates config files, workflows,
* and optionally creates the repo on Gitea via API.
*
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/145
*/
class RepoWizard extends CliFramework
{
private const PLATFORMS = [
'joomla' => 'Joomla extension (component, module, plugin, package)',
'dolibarr' => 'Dolibarr ERP module',
'nodejs' => 'Node.js / TypeScript project',
'python' => 'Python project',
'mcp-server' => 'MCP server (Model Context Protocol)',
'generic' => 'Generic PHP or multi-language project',
];
private const LICENSES = [
'GPL-3.0-or-later' => 'GNU General Public License v3',
'MIT' => 'MIT License',
'Apache-2.0' => 'Apache License 2.0',
'proprietary' => 'Proprietary / All rights reserved',
];
/** Collected wizard answers. */
private array $answers = [];
/** When true, skip all interactive prompts and use defaults. */
private bool $nonInteractive = false;
protected function configure(): void
{
$this->setDescription('Interactive configuration wizard for new repositories');
$this->addArgument('--path', 'Directory to generate files in', '.');
$this->addArgument('--create-remote', 'Create repo on Gitea via API', false);
$this->addArgument('--non-interactive', 'Use defaults (no prompts)', false);
}
protected function run(): int
{
$rawPath = $this->getArgument('--path', '.');
$targetPath = realpath($rawPath) ?: $rawPath;
$this->nonInteractive = (bool) $this->getArgument('--non-interactive', false);
// Validate target path
if (!is_dir($targetPath) && !@mkdir($targetPath, 0755, true)) {
$this->log('ERROR', "Target path does not exist and cannot be created: {$targetPath}");
return self::EXIT_USAGE;
}
$targetPath = realpath($targetPath) ?: $targetPath;
$this->section('MokoCli Repository Wizard');
// ── Gather info ──────────────────────────────────────────────
$this->answers['name'] = $this->ask('Repository name', basename($targetPath));
$this->answers['platform'] = $this->choose('Platform type', self::PLATFORMS, 'generic');
$this->answers['org'] = $this->ask('Organization', 'MokoConsulting');
$this->answers['description'] = $this->ask('Description', '');
$this->answers['license'] = $this->choose('License', self::LICENSES, 'GPL-3.0-or-later');
// ── Confirm ──────────────────────────────────────────────────
$this->section('Configuration Summary');
foreach ($this->answers as $key => $value) {
$this->log('INFO', sprintf(' %-12s %s', $key . ':', $value));
}
if (!$this->confirm('Proceed with these settings?', true)) {
$this->log('INFO', 'Wizard cancelled');
return 0;
}
// ── Generate files ───────────────────────────────────────────
$this->section('Generating files');
$generated = $this->generateFiles($targetPath);
foreach ($generated as $file) {
$this->status(true, $file);
}
// ── Create remote repo ───────────────────────────────────────
if ($this->getArgument('--create-remote', false)) {
$this->section('Creating remote repository');
$this->createRemoteRepo();
}
$this->log('INFO', '');
$this->log('INFO', 'Generated ' . count($generated) . " files in {$targetPath}");
$this->log('INFO', 'Next: git init && git add -A && git commit -m "chore: initial scaffold"');
return 0;
}
// ── File generation ──────────────────────────────────────────────
private function generateFiles(string $path): array
{
$platform = $this->answers['platform'];
$name = $this->answers['name'];
$generated = [];
// .editorconfig
$generated[] = $this->writeFile($path, '.editorconfig', $this->editorconfig());
// README.md
$generated[] = $this->writeFile($path, 'README.md', $this->readme());
// CHANGELOG.md
$generated[] = $this->writeFile($path, 'CHANGELOG.md', $this->changelog());
// LICENSE
if ($this->answers['license'] !== 'proprietary') {
$generated[] = $this->writeFile($path, 'LICENSE', "See SPDX: {$this->answers['license']}");
}
// Platform-specific configs
switch ($platform) {
case 'joomla':
case 'dolibarr':
case 'generic':
$generated[] = $this->writeFile($path, 'phpcs.xml', $this->phpcsXml());
$generated[] = $this->writeFile($path, 'phpstan.neon', $this->phpstanNeon());
$generated[] = $this->writeFile($path, 'composer.json', $this->composerJson());
break;
case 'nodejs':
case 'mcp-server':
$generated[] = $this->writeFile($path, 'package.json', $this->packageJson());
$generated[] = $this->writeFile($path, 'tsconfig.json', $this->tsconfigJson());
$generated[] = $this->writeFile($path, '.eslintrc.json', $this->eslintrc());
break;
case 'python':
$generated[] = $this->writeFile($path, 'pyproject.toml', $this->pyprojectToml());
$generated[] = $this->writeFile($path, 'requirements.txt', '');
break;
}
// .mokogitea/workflows
$generated[] = $this->writeFile($path, '.mokogitea/workflows/pr-check.yml',
"# PR check workflow — synced from mokocli templates\n# Run: moko sync to update\n");
// .gitignore
$generated[] = $this->writeFile($path, '.gitignore', $this->gitignore($platform));
// Source directory
$srcDir = in_array($platform, ['joomla', 'dolibarr', 'generic']) ? 'source' : 'src';
if (!is_dir("{$path}/{$srcDir}")) {
@mkdir("{$path}/{$srcDir}", 0755, true);
$generated[] = "{$srcDir}/";
}
return array_filter($generated);
}
private function writeFile(string $basePath, string $relativePath, string $content): ?string
{
$fullPath = $basePath . '/' . $relativePath;
$dir = dirname($fullPath);
if (file_exists($fullPath)) {
$this->log('DEBUG', " SKIP {$relativePath} (already exists)");
return null;
}
if ($this->dryRun) {
$this->log('INFO', "[dry-run] Would create {$relativePath}");
return $relativePath;
}
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
file_put_contents($fullPath, $content);
return $relativePath;
}
// ── Remote repo creation ─────────────────────────────────────────
private function createRemoteRepo(): void
{
try {
$config = Config::load();
$adapter = PlatformAdapterFactory::create($config);
$org = $this->answers['org'];
if ($this->dryRun) {
$this->log('INFO', "[dry-run] Would create {$org}/{$this->answers['name']} on Gitea");
return;
}
$result = $adapter->createRepository($org, $this->answers['name'], [
'description' => $this->answers['description'],
'private' => false,
]);
$url = $result['html_url'] ?? "{$org}/{$this->answers['name']}";
$this->log('INFO', "Created: {$url}");
} catch (\Exception $e) {
$this->log('ERROR', "Failed to create remote repo: {$e->getMessage()}");
}
}
// ── Interactive helpers (respect --non-interactive) ─────────────
private function ask(string $prompt, string $default): string
{
if ($this->nonInteractive) {
return $default;
}
return $this->input($prompt, $default);
}
private function choose(string $prompt, array $options, string $default): string
{
if ($this->nonInteractive) {
return $default;
}
$keys = array_keys($options);
$labels = [];
foreach ($options as $key => $desc) {
$labels[] = "{$key}{$desc}";
}
$chosen = $this->select($prompt, $labels);
// Extract the key from "key — description"
$chosenKey = explode(' — ', $chosen, 2)[0] ?? $default;
return in_array($chosenKey, $keys, true) ? $chosenKey : $default;
}
// ── Template content ─────────────────────────────────────────────
private function editorconfig(): string
{
return <<<'CONF'
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
CONF;
}
private function readme(): string
{
$name = $this->answers['name'];
$desc = $this->answers['description'] ?: 'A Moko Consulting project.';
$license = $this->answers['license'];
return <<<MD
# {$name}
{$desc}
## License
{$license}
MD;
}
private function changelog(): string
{
return <<<MD
# Changelog
## [Unreleased]
### Added
- Initial project scaffold
MD;
}
private function composerJson(): string
{
$data = [
'name' => 'mokoconsulting/' . strtolower($this->answers['name']),
'description' => $this->answers['description'] ?: $this->answers['name'],
'type' => 'library',
'license' => $this->answers['license'],
'require' => ['php' => '>=8.1'],
'autoload' => ['psr-4' => new \stdClass()],
];
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
}
private function phpcsXml(): string
{
return <<<'XML'
<?xml version="1.0"?>
<ruleset name="MokoCli">
<rule ref="PSR12"/>
<file>source/</file>
<exclude-pattern>vendor/*</exclude-pattern>
</ruleset>
XML;
}
private function phpstanNeon(): string
{
return <<<'NEON'
parameters:
level: 6
paths:
- source/
NEON;
}
private function packageJson(): string
{
$data = [
'name' => '@mokoconsulting/' . strtolower($this->answers['name']),
'version' => '0.1.0',
'description' => $this->answers['description'] ?: $this->answers['name'],
'type' => 'module',
'scripts' => ['build' => 'tsc', 'start' => 'node dist/index.js'],
'devDependencies' => ['typescript' => '^5.0'],
];
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
}
private function tsconfigJson(): string
{
return <<<'JSON'
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"declaration": true
},
"include": ["src/**/*"]
}
JSON;
}
private function eslintrc(): string
{
return <<<'JSON'
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"]
}
JSON;
}
private function pyprojectToml(): string
{
$name = strtolower($this->answers['name']);
$desc = str_replace(['\\', '"'], ['\\\\', '\\"'], $this->answers['description'] ?: $this->answers['name']);
return <<<TOML
[project]
name = "{$name}"
version = "0.1.0"
description = "{$desc}"
requires-python = ">=3.10"
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"
TOML;
}
private function gitignore(string $platform): string
{
$common = <<<'GI'
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
desktop.ini
# Logs
*.log
GI;
$extra = match ($platform) {
'joomla', 'dolibarr', 'generic' => "\n# PHP\nvendor/\n.phpunit.result.cache\n",
'nodejs', 'mcp-server' => "\n# Node\nnode_modules/\ndist/\n*.tsbuildinfo\n",
'python' => "\n# Python\n__pycache__/\n*.pyc\n.venv/\n*.egg-info/\n",
default => '',
};
return $common . $extra;
}
}
$app = new RepoWizard('repo_wizard');
exit($app->execute());
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/scaffold_client.php * PATH: /cli/scaffold_client.php
* VERSION: 09.38.00 * VERSION: 09.24.00
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings * BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
*/ */
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
class ScaffoldClientCli extends CliFramework class ScaffoldClientCli extends CliFramework
{ {
+7 -7
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/sync_rulesets.php * PATH: /cli/sync_rulesets.php
* BRIEF: Apply branch protection rules to all repos via platform adapter * BRIEF: Apply branch protection rules to all repos via platform adapter
*/ */
@@ -20,9 +20,9 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\CliFramework; use MokoEnterprise\CliFramework;
use MokoCli\Config; use MokoEnterprise\Config;
use MokoCli\PlatformAdapterFactory; use MokoEnterprise\PlatformAdapterFactory;
class SyncRulesetsCli extends CliFramework class SyncRulesetsCli extends CliFramework
{ {
@@ -46,7 +46,7 @@ class SyncRulesetsCli extends CliFramework
); );
$platformName = $adapter->getPlatformName(); $platformName = $adapter->getPlatformName();
$ALWAYS_EXCLUDE = ['mokocli', '.github-private']; $ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
// -- Protection rules (platform-agnostic format) -- // -- Protection rules (platform-agnostic format) --
$PROTECTIONS = [ $PROTECTIONS = [
+12 -7
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/theme_lint.php * PATH: /cli/theme_lint.php
* BRIEF: Lint theme files -- CSS syntax, image sizes, hardcoded URLs * BRIEF: Lint theme files -- CSS syntax, image sizes, hardcoded URLs
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
class ThemeLintCli extends CliFramework class ThemeLintCli extends CliFramework
{ {
@@ -41,12 +41,17 @@ class ThemeLintCli extends CliFramework
$errors = 0; $errors = 0;
$warnings = 0; $warnings = 0;
$srcDir = SourceResolver::resolveAbsolute($root); $srcDir = null;
foreach (['src', 'htdocs'] as $d) {
if (is_dir("{$root}/{$d}")) {
$srcDir = "{$root}/{$d}";
break;
}
}
if ($srcDir === null) { if ($srcDir === null) {
$this->log('ERROR', "No source/ or src/ directory in {$root}"); $this->log('ERROR', "No src/ or htdocs/ directory in {$root}");
return 1; return 1;
} }
SourceResolver::warnIfLegacy($root);
echo "Theme Lint: {$srcDir}\n\n"; echo "Theme Lint: {$srcDir}\n\n";
+5 -5
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: mokocli.CLI * DEFGROUP: moko-platform.CLI
* INGROUP: mokocli * INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/updates_xml_build.php * PATH: /cli/updates_xml_build.php
* BRIEF: Generate Joomla updates.xml from extension manifest metadata * BRIEF: Generate Joomla updates.xml from extension manifest metadata
*/ */
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver}; use MokoEnterprise\CliFramework;
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 = SourceResolver::globSource($root, 'pkg_*.xml'); $candidates = glob("{$root}/src/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;

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