Compare commits

..

25 Commits

Author SHA1 Message Date
jmiller 11371cf2d0 feat: add Composer publish workflow for Gitea registry + Packagist
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 46s
2026-06-21 05:45:12 +00:00
jmiller 5948d599f2 chore: rename package to mokoconsulting/mokocli, update description
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 49s
2026-06-21 05:44:48 +00:00
gitea-actions[bot] 7275c8c646 chore: promote changelog [Unreleased] → [09.37.00] 2026-06-21 05:28:23 +00:00
gitea-actions[bot] cb7340ce21 chore(release): build 09.37.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m2s
2026-06-21 05:28:19 +00:00
jmiller a67bf83467 Merge pull request 'feat: interactive repo configuration wizard (#145)' (#294) from feature/145-repo-wizard into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 50s
2026-06-21 05:25:42 +00:00
gitea-actions[bot] 632d8486b8 chore(version): auto-bump patch 09.36.01-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
2026-06-21 05:25:14 +00:00
Jonathan Miller 558cd6043d fix: address review findings in repo_wizard.php
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m2s
- Fix #1: replace nonexistent menu() with choose() using select()
- Fix #2: constructor — pass name only, not description as version
- Fix #3: respect --non-interactive flag (skip prompts, use defaults)
- Fix #4: use json_encode for composer/package.json (prevent injection)
- Fix #5: remove pointless count() wrapper
- Fix #6: validate --path exists or can be created before proceeding
- Fix TOML description escaping
2026-06-21 00:24:50 -05:00
Jonathan Miller 1cf076f088 feat: interactive repo configuration wizard (#145)
Add `repo:wizard` command — walks through platform selection, generates
config files (phpcs, phpstan, eslint, tsconfig, composer/package.json,
.editorconfig, README, CHANGELOG, .gitignore, workflows), and optionally
creates the repo on Gitea via API.

Supports --dry-run, --non-interactive, and --create-remote flags.
2026-06-21 00:24:49 -05:00
jmiller 00f0e44c78 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 04:46:56 +00:00
gitea-actions[bot] c976f400f4 chore: promote changelog [Unreleased] → [09.36.00] 2026-06-21 04:46:41 +00:00
gitea-actions[bot] ebf37423f2 chore(release): build 09.36.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 42s
2026-06-21 04:46:38 +00:00
jmiller e4d836067f Merge pull request 'feat: deploy:verify — deploy with auto health check and rollback (#147)' (#293) from feature/147-deploy-verify into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 49s
2026-06-21 04:46:21 +00:00
gitea-actions[bot] 8900b12f81 chore(version): auto-bump patch 09.35.02-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 20s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 1m28s
2026-06-21 04:42:00 +00:00
Jonathan Miller 4fc3d0a4a9 fix: address review findings in deploy-and-verify.php
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 56s
- Fix #1: replace rm -rf with cross-platform PHP removeDirectory()
- Fix #2: sanitize URL in audit log (log hostname only)
- Fix #3: remove unused buildHealthArgs() and $healthArgs
- Fix #4: add random suffix to snapshot dir name for uniqueness
- Fix #5: fix constructor to match CliFramework pattern (no args)
- Fix #6: trigger rollback on deploy failure (partial deploy risk)
2026-06-20 23:41:09 -05:00
gitea-actions[bot] 19aa0111f0 chore(version): auto-bump patch 09.35.01-dev [skip ci] 2026-06-21 04:31:30 +00:00
Jonathan Miller 46e33a9383 feat: deploy:verify — deploy with auto health check and rollback (#147)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Orchestrates backup → deploy → health-check → rollback-if-failed:
- Pre-deploy snapshot via backup-before-deploy.php
- Deploy via deploy-sftp.php subprocess
- Inline health check with configurable retries and delay
- Auto-rollback via rollback-joomla.php if health check fails
- Post-rollback verification
- Full audit trail via AuditLogger
2026-06-20 23:31:08 -05:00
jmiller 2f43bac247 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 04:10:46 +00:00
gitea-actions[bot] 817e9caee8 chore: promote changelog [Unreleased] → [09.35.00] 2026-06-21 03:22:59 +00:00
gitea-actions[bot] 6216803590 chore(release): build 09.35.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 40s
2026-06-21 03:22:57 +00:00
jmiller caad8ee7d0 Merge pull request 'feat: smart error recovery suggestions in validators (#146)' (#292) from feature/146-recovery-suggestions into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 40s
2026-06-21 03:22:42 +00:00
gitea-actions[bot] 558c0a0edf chore(version): auto-bump patch 09.34.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 16s
2026-06-21 03:20:33 +00:00
Jonathan Miller cb1053274e feat: smart error recovery suggestions in validators (#146)
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 48s
- Add RecoverySuggestion class with methods for common fix patterns
  (missing files, directories, XML elements, version mismatches, commands)
- Add suggest() method to CliFramework (yellow lightbulb-prefixed output)
- Integrate into check_structure.php (missing file/dir suggestions)
- Integrate into check_version_consistency.php (version mismatch fixes)
2026-06-20 22:20:16 -05:00
jmiller 743da9c4c2 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 02:50:31 +00:00
jmiller 4b6fcb5fa4 Merge pull request 'fix(version-bump): prevent dev from falling behind stable (#289)' (#291) from feature/289-cherry-pick-version-bump into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m2s
2026-06-21 02:49:47 +00:00
gitea-actions[bot] 2a45dd873b chore: promote changelog [Unreleased] → [09.34.00] 2026-06-21 02:47:06 +00:00
48 changed files with 1016 additions and 46 deletions
+76
View File
@@ -0,0 +1,76 @@
# 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
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 09.34.01
# VERSION: 09.37.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+7 -5
View File
@@ -12,12 +12,14 @@ BRIEF: Release changelog
# Changelog
## [Unreleased]
## [09.34.00] --- 2026-06-21
## [09.37.00] --- 2026-06-21
## [09.33.00] --- 2026-06-21
## [09.37.00] --- 2026-06-21
## [09.33.00] --- 2026-06-21
## [09.36.00] --- 2026-06-21
## [09.32.00] --- 2026-06-21
## [09.36.00] --- 2026-06-21
## [09.32.00] --- 2026-06-21
## [09.35.00] --- 2026-06-21
## [09.35.00] --- 2026-06-21
+1 -1
View File
@@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root
INGROUP: MokoPlatform
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
PATH: /README.md
VERSION: 09.34.01
VERSION: 09.37.00
BRIEF: Project overview and documentation
-->
+1 -1
View File
@@ -13,7 +13,7 @@
* INGROUP: MokoPlatform.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /automation/update_dependencies.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Cross-repo dependency update automation — scan, update, PR, auto-merge
*/
+2
View File
@@ -178,6 +178,7 @@ const COMMAND_MAP = [
'repo:archive' => 'cli/archive_repo.php',
'repo:scaffold-client' => 'cli/scaffold_client.php',
'repo:provision' => 'cli/client_provision.php',
'repo:wizard' => 'cli/repo_wizard.php',
'repo:rename-branch' => 'cli/branch_rename.php',
'repo:reset-dev' => 'cli/dev_branch_reset.php',
@@ -199,6 +200,7 @@ const COMMAND_MAP = [
'deploy:sftp' => 'deploy/deploy-sftp.php',
'deploy:backup' => 'deploy/backup-before-deploy.php',
'deploy:health-check' => 'deploy/health-check.php',
'deploy:verify' => 'deploy/deploy-and-verify.php',
'deploy:rollback' => 'deploy/rollback-joomla.php',
'deploy:sync' => 'deploy/sync-joomla.php',
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/branch_rename.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/bulk_workflow_push.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/bulk_workflow_trigger.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Trigger a workflow across multiple repos at once
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/client_dashboard.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Generate unified client dashboard HTML
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/client_inventory.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Discover and list all client-waas repos with their server configuration status
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/client_provision.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Provision a new client environment end-to-end
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/grafana_dashboard.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Manage Grafana dashboards via API
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/joomla_build.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
* NOTE: Called by pre-release and auto-release workflows.
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/joomla_metadata_validate.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/manifest_detect.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/manifest_integrity.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Cross-check manifest API fields against repo contents across the org
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/manifest_licensing.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_read.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/platform_detect.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Auto-detect repository platform type and optionally update manifest
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_cascade.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Cascade release zip to all lower stability channels
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/release_publish.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Publish a release and create copies for all lesser stability streams.
*/
+429
View File
@@ -0,0 +1,429 @@
#!/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());
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/scaffold_client.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/updates_xml_sync.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Sync updates.xml to target branches via Gitea API
* NOTE: Called by pre-release and auto-release workflows after updates.xml
* is modified on the current branch. Pushes the file to other branches
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/version_auto_bump.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
*/
+1 -1
View File
@@ -370,7 +370,7 @@ class VersionBumpCli extends CliFramework
/**
* Scan git release tags for the highest version across all channels.
*
* Checks release names like "MokoSuiteClient (VERSION: 09.34.01)" in
* Checks release names like "MokoSuiteClient (VERSION: 09.37.00)" in
* git tags (stable, release-candidate, development, etc.) to find the
* highest version that has been released on any channel.
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/version_check.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Validate version consistency across README, manifests, and sub-packages
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/wiki_sync.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Sync select wiki pages from mokoplatform to all template repos
*/
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/workflow_sync.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
*/
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "mokoconsulting-tech/enterprise",
"description": "mokoplatform Enterprise API \u2014 PHP implementation",
"name": "mokoconsulting/mokocli",
"description": "MokoCLI — enterprise CLI automation, validation, and governance engine for all Moko Consulting repositories",
"type": "library",
"version": "09.23.00",
"license": "GPL-3.0-or-later",
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/backup-before-deploy.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
*/
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Scripts.Deploy
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /deploy/deploy-and-verify.php
* BRIEF: Deploy with automatic health check and rollback on failure
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{AuditLogger, CliFramework};
/**
* Deploy-and-Verify: orchestrates backup → deploy → health-check → rollback.
*
* If the health check fails after deployment, automatically triggers a rollback
* using the pre-deploy snapshot, with full audit trail.
*
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/147
*/
class DeployAndVerify extends CliFramework
{
private ?AuditLogger $auditLogger = null;
protected function configure(): void
{
$this->setDescription('Deploy with automatic health check and rollback on failure');
$this->addArgument('--path', 'Repository root', '.');
$this->addArgument('--env', 'Target environment: dev, demo, rs, live', '');
$this->addArgument('--config', 'Explicit sftp-config path (overrides --env)', '');
$this->addArgument('--url', 'Site URL for health check', '');
$this->addArgument('--checks', 'Health checks: http,admin,api (comma-sep)', 'http');
$this->addArgument('--timeout', 'Health check timeout in seconds', '30');
$this->addArgument('--retries', 'Health check retries before rollback', '2');
$this->addArgument('--delay', 'Seconds between health check retries', '5');
}
protected function run(): int
{
$path = realpath($this->getArgument('--path', '.')) ?: '.';
$env = $this->getArgument('--env', '');
$config = $this->getArgument('--config', '');
$url = $this->getArgument('--url', '');
$checks = $this->getArgument('--checks', 'http');
$timeout = (int) $this->getArgument('--timeout', '30');
$retries = (int) $this->getArgument('--retries', '2');
$delay = (int) $this->getArgument('--delay', '5');
if ($url === '') {
$this->log('ERROR', 'The --url argument is required for health checks');
return self::EXIT_USAGE;
}
if ($env === '' && $config === '') {
$this->log('ERROR', 'Specify --env or --config for the deploy target');
return self::EXIT_USAGE;
}
try {
$this->auditLogger = new AuditLogger('deploy-and-verify');
} catch (\Exception $e) {
// Non-fatal — proceed without audit logging
}
$this->audit('start', ['path' => $path, 'env' => $env, 'url' => parse_url($url, PHP_URL_HOST) ?? $url]);
// ── Build subprocess args ────────────────────────────────────
$deployArgs = $this->buildDeployArgs($path, $env, $config);
// ── Step 1: Backup ───────────────────────────────────────────
$this->section('Step 1: Pre-deploy backup');
$snapshotDir = sys_get_temp_dir() . '/moko_deploy_snapshot_' . date('Ymd_His') . '_' . getmypid() . '_' . bin2hex(random_bytes(4));
if ($this->dryRun) {
$this->log('INFO', "[dry-run] Would create snapshot at {$snapshotDir}");
} else {
$backupExit = $this->runSubprocess('backup-before-deploy.php', array_merge(
$deployArgs, ['--snapshot-dir', $snapshotDir]
));
if ($backupExit !== 0) {
$this->log('ERROR', 'Pre-deploy backup failed — aborting deployment');
$this->audit('backup_failed', ['exit_code' => $backupExit]);
return self::EXIT_FAILURE;
}
$this->log('INFO', "Snapshot saved to {$snapshotDir}");
}
// ── Step 2: Deploy ───────────────────────────────────────────
$this->section('Step 2: Deploy');
if ($this->dryRun) {
$this->log('INFO', '[dry-run] Would run deploy-sftp.php ' . implode(' ', $deployArgs));
} else {
$deployExit = $this->runSubprocess('deploy-sftp.php', $deployArgs);
if ($deployExit !== 0) {
$this->log('ERROR', 'Deploy failed — rolling back to pre-deploy state');
$this->audit('deploy_failed', ['exit_code' => $deployExit]);
$this->runSubprocess('rollback-joomla.php', array_merge(
$deployArgs, ['--snapshot-dir', $snapshotDir]
));
$this->cleanup($snapshotDir);
return self::EXIT_FAILURE;
}
$this->log('INFO', 'Deploy completed successfully');
}
// ── Step 3: Health check (with retries) ──────────────────────
$this->section('Step 3: Health check');
if ($this->dryRun) {
$this->log('INFO', "[dry-run] Would check {$url} with checks: {$checks}");
$this->log('INFO', '[dry-run] Deploy-and-verify complete');
return self::EXIT_SUCCESS;
}
$healthy = false;
for ($attempt = 1; $attempt <= $retries; $attempt++) {
$this->log('INFO', "Health check attempt {$attempt}/{$retries}...");
if ($attempt > 1) {
$this->log('INFO', "Waiting {$delay}s before retry...");
sleep($delay);
}
$healthExit = $this->runHealthCheck($url, $checks, $timeout);
if ($healthExit === 0) {
$healthy = true;
break;
}
$this->log('WARNING', "Health check attempt {$attempt} failed (exit {$healthExit})");
}
if ($healthy) {
$this->section('Result: SUCCESS');
$this->log('INFO', 'Health check passed — deploy verified');
$this->audit('success', ['url' => $url, 'attempts' => $attempt]);
$this->cleanup($snapshotDir);
return self::EXIT_SUCCESS;
}
// ── Step 4: Rollback ─────────────────────────────────────────
$this->section('Step 4: ROLLBACK');
$this->log('ERROR', "Health check failed after {$retries} attempts — rolling back");
$this->audit('rollback_triggered', ['url' => $url, 'retries' => $retries]);
$rollbackExit = $this->runSubprocess('rollback-joomla.php', array_merge(
$deployArgs, ['--snapshot-dir', $snapshotDir]
));
if ($rollbackExit === 0) {
$this->log('INFO', 'Rollback completed — site restored to pre-deploy state');
$this->audit('rollback_success', []);
// Verify rollback worked
$postRollbackHealth = $this->runHealthCheck($url, $checks, $timeout);
if ($postRollbackHealth === 0) {
$this->log('INFO', 'Post-rollback health check passed — site is healthy');
} else {
$this->log('ERROR', 'Post-rollback health check FAILED — manual intervention needed');
$this->audit('rollback_verification_failed', []);
}
} else {
$this->log('ERROR', 'Rollback FAILED — manual intervention required');
$this->audit('rollback_failed', ['exit_code' => $rollbackExit]);
}
$this->cleanup($snapshotDir);
return self::EXIT_FAILURE;
}
// ── Health check (inline, no subprocess) ─────────────────────────
private function runHealthCheck(string $url, string $checks, int $timeout): int
{
$url = rtrim($url, '/');
$checkList = array_map('trim', explode(',', $checks));
$failed = 0;
foreach ($checkList as $check) {
$checkUrl = match ($check) {
'admin' => $url . '/administrator/',
'api' => $url . '/api/index.php/v1',
default => $url,
};
$result = $this->httpGet($checkUrl, $timeout);
if ($result === null) {
$this->log('ERROR', " [{$check}] FAIL: connection failed");
$failed++;
continue;
}
$validCodes = ($check === 'api') ? [200, 401] : [200];
if (!in_array($result['http_code'], $validCodes, true)) {
$this->log('ERROR', " [{$check}] FAIL: HTTP {$result['http_code']}");
$failed++;
continue;
}
if ($this->containsFatalError($result['body'])) {
$this->log('ERROR', " [{$check}] FAIL: PHP fatal error in response");
$failed++;
continue;
}
$this->log('INFO', " [{$check}] PASS: HTTP {$result['http_code']} ({$result['time_ms']}ms)");
}
return $failed > 0 ? 1 : 0;
}
private function httpGet(string $url, int $timeout): ?array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_USERAGENT => 'MokoDeployVerify/1.0',
]);
$body = curl_exec($ch);
if (curl_errno($ch)) {
curl_close($ch);
return null;
}
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
curl_close($ch);
return [
'http_code' => $httpCode,
'body' => is_string($body) ? $body : '',
'time_ms' => (int) round($totalTime * 1000),
];
}
private function containsFatalError(string $body): bool
{
foreach (['Fatal error:', 'Fatal Error', 'Parse error:', 'Uncaught Error:', 'Uncaught Exception:'] as $pattern) {
if (stripos($body, $pattern) !== false) {
return true;
}
}
return false;
}
// ── Subprocess helpers ───────────────────────────────────────────
private function runSubprocess(string $script, array $args): int
{
$scriptPath = __DIR__ . '/' . $script;
if (!is_file($scriptPath)) {
$this->log('ERROR', "Script not found: {$scriptPath}");
return 127;
}
$cmd = sprintf('php %s %s 2>&1',
escapeshellarg($scriptPath),
implode(' ', array_map('escapeshellarg', $args))
);
$this->log('DEBUG', "Running: {$cmd}");
passthru($cmd, $exitCode);
return $exitCode;
}
private function buildDeployArgs(string $path, string $env, string $config): array
{
$args = ['--path', $path];
if ($config !== '') {
$args[] = '--config';
$args[] = $config;
} elseif ($env !== '') {
$args[] = '--env';
$args[] = $env;
}
if ($this->dryRun) {
$args[] = '--dry-run';
}
return $args;
}
// ── Audit ────────────────────────────────────────────────────────
private function audit(string $event, array $data): void
{
if ($this->auditLogger === null) {
return;
}
try {
$this->auditLogger->logInfo("deploy-verify:{$event}", $data);
} catch (\Exception $e) {
// Non-fatal
}
}
// ── Cleanup ──────────────────────────────────────────────────────
private function cleanup(string $snapshotDir): void
{
if (is_dir($snapshotDir)) {
$this->removeDirectory($snapshotDir);
$this->log('DEBUG', "Cleaned up snapshot: {$snapshotDir}");
}
}
private function removeDirectory(string $dir): void
{
$entries = scandir($dir);
if ($entries === false) {
return;
}
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $entry;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
}
$app = new DeployAndVerify();
exit($app->execute());
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/deploy-dolibarr.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/health-check.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Post-deploy health check — verify a Joomla site is responding correctly
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/rollback-joomla.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/sync-joomla.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Sync Joomla site directories between two servers via rsync over SSH
*/
+19
View File
@@ -590,6 +590,25 @@ abstract class CliFramework
$this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n");
}
/**
* Display a recovery suggestion with a lightbulb prefix.
*
* @param string $suggestion Fix suggestion text (from RecoverySuggestion)
*/
protected function suggest(string $suggestion): void
{
if ($this->quiet) {
return;
}
$this->clearProgress();
$lines = explode("\n", $suggestion);
$first = array_shift($lines);
$this->display(' ' . $this->c(self::C_YELLOW, '💡 ' . $first) . "\n");
foreach ($lines as $line) {
$this->display(' ' . $this->c(self::C_YELLOW, ' ' . $line) . "\n");
}
}
// =========================================================================
// Console graphics — progress bar
// =========================================================================
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Enterprise
* INGROUP: MokoPlatform.Lib
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /lib/Enterprise/RecoverySuggestion.php
* BRIEF: Smart error recovery suggestions for validators
*/
declare(strict_types=1);
namespace MokoCli;
/**
* Generates actionable fix suggestions when validators detect problems.
*
* Each method returns a human-readable suggestion string that tells the
* developer exactly what to do to fix the issue.
*/
class RecoverySuggestion
{
/**
* Suggest creating a missing required file.
*/
public static function forMissingFile(string $file, string $template = ''): string
{
$suggestion = "Create the missing file: {$file}";
if ($template !== '') {
$suggestion .= "\n Copy from template: {$template}";
}
return $suggestion;
}
/**
* Suggest adding a missing XML element.
*/
public static function forMissingXmlElement(string $element, string $value, string $file, int $afterLine = 0): string
{
$snippet = "<{$element}>{$value}</{$element}>";
if ($afterLine > 0) {
return "Add {$snippet} after line {$afterLine} in {$file}";
}
return "Add {$snippet} to {$file}";
}
/**
* Suggest fixing a version mismatch.
*/
public static function forVersionMismatch(string $file, string $found, string $expected): string
{
return "Update version in {$file}: change \"{$found}\" to \"{$expected}\"";
}
/**
* Suggest creating a missing directory.
*/
public static function forMissingDirectory(string $dir): string
{
return "Create the missing directory:\n mkdir -p {$dir}";
}
/**
* Suggest fixing a syntax error.
*/
public static function forSyntaxError(string $file, int $line, string $error): string
{
return "Fix syntax error at {$file}:{$line}\n {$error}";
}
/**
* Suggest fixing a missing license header.
*/
public static function forMissingHeader(string $file): string
{
return "Add SPDX license header to {$file}:\n /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n * SPDX-License-Identifier: GPL-3.0-or-later */";
}
/**
* Suggest running a command to fix an issue.
*/
public static function forCommand(string $command, string $context = ''): string
{
$suggestion = "Run: {$command}";
if ($context !== '') {
$suggestion = "{$context}\n {$suggestion}";
}
return $suggestion;
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: dolibarr-api-mcp.Documentation
INGROUP: dolibarr-api-mcp
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
VERSION: 09.34.01
VERSION: 09.37.00
PATH: ./CONTRIBUTING.md
BRIEF: Contribution guidelines for the project
-->
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: dolibarr-api-mcp.Documentation
INGROUP: dolibarr-api-mcp
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
PATH: /SECURITY.md
VERSION: 09.34.01
VERSION: 09.37.00
BRIEF: Security vulnerability reporting and handling policy
-->
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP:
INGROUP: Project.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli-Template-Generic
VERSION: 09.34.01
VERSION: 09.37.00
PATH: ./CONTRIBUTING.md
BRIEF: Contribution guidelines for the project
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 09.34.01
VERSION: 09.37.00
BRIEF: Security vulnerability reporting and handling policy
-->
+1 -1
View File
@@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase
{
file_put_contents(
"{$this->tmpDir}/README.md",
"<!-- VERSION: 09.34.01 -->\nSome content\n"
"<!-- VERSION: 09.37.00 -->\nSome content\n"
);
$this->execute();
+2 -2
View File
@@ -34,7 +34,7 @@ class VersionReadTest extends TestCase
{
file_put_contents(
"{$this->tmpDir}/README.md",
"# Test\n<!-- VERSION: 09.34.01 -->\n"
"# Test\n<!-- VERSION: 09.37.00 -->\n"
);
$this->assertSame('02.03.04', trim($this->runScript()));
@@ -68,7 +68,7 @@ class VersionReadTest extends TestCase
{
file_put_contents(
"{$this->tmpDir}/README.md",
"<!-- VERSION: 09.34.01 -->\n"
"<!-- VERSION: 09.37.00 -->\n"
);
mkdir("{$this->tmpDir}/src", 0755, true);
file_put_contents(
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /validate/check_file_integrity.php
* VERSION: 09.34.01
* VERSION: 09.37.00
* BRIEF: Compare deployed files on a remote server against the local repository to detect drift
*/
+3
View File
@@ -20,6 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoCli\CliFramework;
use MokoCli\RecoverySuggestion;
/**
* Validates that the required directories and files exist in the repository root.
@@ -67,6 +68,7 @@ class CheckStructure extends CliFramework
if (!is_dir($path . '/' . $dir)) {
$missingDirs[] = $dir;
$this->status(false, "Directory: {$dir}");
$this->suggest(RecoverySuggestion::forMissingDirectory($dir));
$failed++;
} else {
$this->status(true, "Directory: {$dir}");
@@ -96,6 +98,7 @@ class CheckStructure extends CliFramework
if (!is_file($path . '/' . $file)) {
$missingFiles[] = $file;
$this->status(false, "File: {$file}");
$this->suggest(RecoverySuggestion::forMissingFile($file));
$failed++;
} else {
$this->status(true, "File: {$file}");
+2
View File
@@ -21,6 +21,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoCli\CliFramework;
use MokoCli\RecoverySuggestion;
/**
* Checks that the version recorded in composer.json matches VERSION headers
@@ -101,6 +102,7 @@ class CheckVersionConsistency extends CliFramework
if ($match[0] !== $expected) {
$line = substr_count(substr($content, 0, (int) $match[1]), "\n") + 1;
$this->status(false, $filename, "line {$line}: found {$match[0]}, expected {$expected}");
$this->suggest(RecoverySuggestion::forVersionMismatch($filename, $match[0], $expected));
$issues[] = $filename;
$filePassed = false;
}