Compare commits

...

19 Commits

Author SHA1 Message Date
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
Jonathan Miller e7b2c1fba2 fix(version-bump): prevent dev from falling behind stable (#289)
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 / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m6s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 2m2s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 28s
Cherry-pick from dev (d5fa609). version_bump.php now checks:
1. --min-version argument from workflow
2. Auto-detect from git — scans origin/main and origin/rc for highest
   released version and uses it as the bump base

Closes #289
2026-06-20 21:49:17 -05:00
gitea-actions[bot] 2a45dd873b chore: promote changelog [Unreleased] → [09.34.00] 2026-06-21 02:47:06 +00:00
gitea-actions[bot] e0f1ec1372 chore(release): build 09.34.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 1m6s
2026-06-21 02:47:03 +00:00
jmiller f325de91d4 Merge pull request 'feat: cross-repo dependency update automation (#149)' (#290) from feature/149-dependency-update-automation 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 1m3s
2026-06-21 02:46:47 +00:00
gitea-actions[bot] 109493ab4a chore(version): auto-bump patch 09.33.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 19s
2026-06-21 02:42:15 +00:00
Jonathan Miller 0d3b14d55c feat: cross-repo dependency update automation (#149)
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Add `deps:update` command that scans org repos for outdated Composer/npm
dependencies, creates PRs with changelogs, and optionally auto-merges
safe patch updates.

- Composer: runs `composer outdated --format=json`, updates targeted packages
- npm: runs `npm outdated --json`, updates targeted packages
- Skips repos with existing deps PRs (no duplicates)
- Checkpoint-based resumability with --resume
- --patch-only for safe updates, --auto-merge for patch PRs
- Supports --repos and --exclude filters
2026-06-20 21:41:43 -05:00
jmiller 35075aa743 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 02:23:11 +00:00
gitea-actions[bot] 7be017ae30 chore: promote changelog [Unreleased] → [09.33.00] 2026-06-21 02:22:56 +00:00
gitea-actions[bot] e8a3414ff4 chore(release): build 09.33.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 1m25s
2026-06-21 02:22:53 +00:00
jmiller e8697c2d0e Merge pull request 'feat: deploy-sftp.php --env demo/live + multi-instance (#184)' (#287) from feature/184-deploy-env-demo-live 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 1m23s
2026-06-21 02:22:39 +00:00
gitea-actions[bot] 7d369628f0 chore(version): auto-bump patch 09.32.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 16s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 1m57s
2026-06-21 02:21:55 +00:00
Jonathan Miller e834b8a3ea feat: deploy-sftp.php supports --env demo and --env live with multi-instance (#184)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
- Add demo and live to ENV_CONFIG_MAP
- Add multi-target deploy via LIVE_TARGETS env var (JSON array of targets)
- Add sftp-config.demo.json.example and sftp-config.live.json.example templates
- Failed targets logged but don't block remaining deploys
2026-06-20 21:21:42 -05:00
47 changed files with 1073 additions and 65 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 09.32.00
# VERSION: 09.35.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+6 -24
View File
@@ -12,32 +12,14 @@ BRIEF: Release changelog
# Changelog
## [Unreleased]
## [09.32.00] --- 2026-06-21
## [09.35.00] --- 2026-06-21
## [09.32.00] --- 2026-06-21
## [09.35.00] --- 2026-06-21
## [09.31.00] --- 2026-06-21
## [09.34.00] --- 2026-06-21
## [09.31.00] --- 2026-06-21
## [09.34.00] --- 2026-06-21
## [09.30.00] --- 2026-06-21
## [09.33.00] --- 2026-06-21
## [09.30.00] --- 2026-06-21
### Added
- `security:advisories` command — cross-repo security advisory aggregator (#150)
- Scans org repos for known CVEs via `composer audit`
- Aggregates results into a single report with severity breakdown
- Auto-creates tracking issues for critical/high vulnerabilities (`--create-issues`)
- Checkpoint-based resumability with `--resume`
- Export to JSON/CSV with `--export`
### Changed
- `manifest:read` rewritten to use Gitea manifest API as primary source (#283)
- Falls back to auto-detection from source tree (Joomla, Dolibarr, generic)
- No longer requires `.mokogitea/manifest.xml` file
- Backward-compatible field aliases for existing CI consumers
- Renamed `MokoStandards` namespace → `MokoCli` across all files
- Renamed `MokoEnterprise` namespace → `MokoCli` across all files
- Renamed `MokoStandardsParser` class → `ManifestParser`
- Fixed `composer.json` autoload paths: `src/``source/`
## [09.33.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.32.00
VERSION: 09.35.00
BRIEF: Project overview and documentation
-->
+633
View File
@@ -0,0 +1,633 @@
#!/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.35.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());
+1
View File
@@ -89,6 +89,7 @@ const COMMAND_MAP = [
// Automation
'sync' => 'automation/bulk_sync.php',
'deps:update' => 'automation/update_dependencies.php',
'automation:cleanup' => 'automation/repo_cleanup.php',
'automation:migrate-gitea' => 'automation/migrate_to_gitea.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.00
* BRIEF: Publish a release and create copies for all lesser stability streams.
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/scaffold_client.php
* VERSION: 09.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.00
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
*/
+71
View File
@@ -27,6 +27,7 @@ class VersionBumpCli extends CliFramework
$this->addArgument('--path', 'Repository root', '.');
$this->addArgument('--minor', 'Bump minor version', false);
$this->addArgument('--major', 'Bump major version', false);
$this->addArgument('--min-version', 'Minimum base version (ensures bump is above this)', '');
}
protected function run(): int
@@ -116,6 +117,28 @@ class VersionBumpCli extends CliFramework
$baseVersion = $v;
}
}
// Check --min-version: ensures dev never falls behind stable
$minVersion = $this->getArgument('--min-version');
if (!empty($minVersion)) {
$minVersion = preg_replace('/-(?:dev|alpha|beta|rc)$/', '', $minVersion);
if (preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $minVersion)) {
if ($baseVersion === null || version_compare($minVersion, $baseVersion, '>')) {
$this->log('INFO', "Using --min-version {$minVersion} (higher than manifest {$baseVersion})");
$baseVersion = $minVersion;
}
}
}
// Auto-detect: scan git tags for higher versions from other channels
if ($baseVersion !== null) {
$gitTagVersion = $this->getHighestGitTagVersion($root);
if ($gitTagVersion !== null && version_compare($gitTagVersion, $baseVersion, '>')) {
$this->log('INFO', "Git tag version {$gitTagVersion} is higher than manifest {$baseVersion} — using as base");
$baseVersion = $gitTagVersion;
}
}
if ($baseVersion === null) {
$this->log('ERROR', "No version found in manifest.xml, README.md, or Joomla XML");
return 1;
@@ -343,6 +366,54 @@ class VersionBumpCli extends CliFramework
echo "{$old} -> {$newFull}\n";
return 0;
}
/**
* Scan git release tags for the highest version across all channels.
*
* Checks release names like "MokoSuiteClient (VERSION: 09.35.00)" in
* git tags (stable, release-candidate, development, etc.) to find the
* highest version that has been released on any channel.
*/
private function getHighestGitTagVersion(string $root): ?string
{
$highest = null;
// Method 1: Parse version from git tag annotations / release commit messages
$output = [];
exec("cd " . escapeshellarg($root) . " && git log --all --oneline --grep='chore(version)' --grep='chore(release)' --format='%s' -20 2>/dev/null", $output);
foreach ($output as $line) {
if (preg_match('/(\d{2}\.\d{2}\.\d{2})/', $line, $m)) {
$v = preg_replace('/-(?:dev|alpha|beta|rc)$/', '', $m[1]);
if ($highest === null || version_compare($v, $highest, '>')) {
$highest = $v;
}
}
}
// Method 2: Check version in remote branches' manifest files
$branches = ['origin/main', 'origin/rc', 'origin/dev'];
$manifestPaths = ['source/pkg_*.xml', 'pkg_*.xml'];
foreach ($branches as $branch) {
foreach ($manifestPaths as $pattern) {
$files = [];
exec("cd " . escapeshellarg($root) . " && git ls-tree --name-only {$branch} -- '{$pattern}' 2>/dev/null", $files);
foreach ($files as $file) {
$content = shell_exec("cd " . escapeshellarg($root) . " && git show {$branch}:{$file} 2>/dev/null");
if ($content && preg_match('#<version>(\d{2}\.\d{2}\.\d{2})(?:-(?:dev|alpha|beta|rc))?</version>#', $content, $m)) {
$v = $m[1];
if ($highest === null || version_compare($v, $highest, '>')) {
$highest = $v;
}
}
}
}
}
return $highest;
}
}
$app = new VersionBumpCli();
+1 -1
View File
@@ -10,7 +10,7 @@
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /cli/version_check.php
* VERSION: 09.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.00
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
*/
+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.32.00
* VERSION: 09.35.00
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
*/
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/deploy-dolibarr.php
* VERSION: 09.32.00
* VERSION: 09.35.00
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
*/
+111 -4
View File
@@ -66,8 +66,16 @@ class DeploySftp extends CliFramework
*/
protected function run(): int
{
$repoPath = $this->resolveRepoPath();
$srcDir = $this->resolveSrcDir($repoPath);
$repoPath = $this->resolveRepoPath();
$srcDir = $this->resolveSrcDir($repoPath);
$env = strtolower($this->getArgument('--env', '') ?: '');
// Multi-target: LIVE_TARGETS env var overrides config file for live deploys
$liveTargets = getenv('LIVE_TARGETS') ?: '';
if ($liveTargets !== '' && ($env === 'live' || $env === '')) {
return $this->deployMultiTarget($repoPath, $srcDir, $liveTargets);
}
$configPath = $this->resolveConfigPath($repoPath);
$this->log("Repository : {$repoPath}");
@@ -130,6 +138,103 @@ class DeploySftp extends CliFramework
return $exitCode;
}
// ─── Multi-target deploy ────────────────────────────────────────────────
/**
* Deploy to multiple live targets from LIVE_TARGETS JSON.
*
* LIVE_TARGETS format (JSON array of objects):
* [
* {"host": "web1.example.com", "user": "deploy", "remote_path": "/var/www/module/", "ssh_key_file": "~/.ssh/id_rsa"},
* {"host": "web2.example.com", "user": "deploy", "remote_path": "/var/www/module/", "ssh_key_file": "~/.ssh/id_rsa"}
* ]
*
* @return int POSIX exit code (0 if all targets succeed)
*/
private function deployMultiTarget(string $repoPath, string $srcDir, string $liveTargetsJson): int
{
$targets = json_decode($liveTargetsJson, true);
if (!is_array($targets) || empty($targets)) {
$this->log('ERROR', 'LIVE_TARGETS is not a valid JSON array');
return self::EXIT_USAGE;
}
$this->section("Multi-target live deploy ({$this->count($targets)} targets)");
$succeeded = 0;
$failed = 0;
foreach ($targets as $i => $target) {
$host = $target['host'] ?? 'unknown';
$this->section("Target " . ($i + 1) . ": {$host}");
// Merge target config into $this->config for this iteration
$this->config = $target;
if (!$this->validateConfig()) {
$this->log('ERROR', "Skipping target {$host} — invalid config");
$failed++;
continue;
}
$remotePath = rtrim((string) $this->config['remote_path'], '/');
$ignores = array_merge(
$this->buildIgnorePatterns(),
$this->loadFtpIgnorePatterns($srcDir),
$this->loadFtpIgnorePatterns($repoPath)
);
$user = (string) $this->config['user'];
$port = (int) ($this->config['port'] ?? 22);
if ($this->dryRun) {
$this->log("[DRY RUN] Would deploy to {$user}@{$host}:{$port}{$remotePath}");
$succeeded++;
continue;
}
$sftp = $this->connect($host, $port, $user, $repoPath);
if ($sftp === null) {
$this->log('ERROR', "Failed to connect to {$host}");
$failed++;
continue;
}
// Reset counters per target
$this->uploaded = 0;
$this->skipped = 0;
$this->unchanged = 0;
$this->deleted = 0;
$dirCheck = @$sftp->nlist(dirname($remotePath));
$baseName = basename($remotePath);
$dirExists = is_array($dirCheck) && in_array($baseName, $dirCheck, true);
if (!$dirExists) {
$sftp->mkdir($remotePath, -1, true);
}
$exitCode = $this->uploadDirectory($sftp, $srcDir, $remotePath, $srcDir, $ignores);
$this->log(" {$host}: Uploaded={$this->uploaded} Unchanged={$this->unchanged} Deleted={$this->deleted} Skipped={$this->skipped}");
if ($exitCode === 0) {
$succeeded++;
} else {
$failed++;
}
}
$this->section('Multi-target summary');
$this->log("Succeeded: {$succeeded}, Failed: {$failed}");
return $failed > 0 ? self::EXIT_FAILURE : self::EXIT_SUCCESS;
}
private function count(array $arr): int
{
return \count($arr);
}
// ─── Private helpers ──────────────────────────────────────────────────────
/**
@@ -171,8 +276,10 @@ class DeploySftp extends CliFramework
/** Map of --env values to their sftp-config filename. */
private const ENV_CONFIG_MAP = [
'dev' => 'sftp-config.dev.json',
'rs' => 'sftp-config.rs.json',
'dev' => 'sftp-config.dev.json',
'rs' => 'sftp-config.rs.json',
'demo' => 'sftp-config.demo.json',
'live' => 'sftp-config.live.json',
];
/**
+1 -1
View File
@@ -12,7 +12,7 @@
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
* PATH: /deploy/health-check.php
* VERSION: 09.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
* VERSION: 09.35.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.32.00
VERSION: 09.35.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.32.00
VERSION: 09.35.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.32.00
VERSION: 09.35.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.32.00
VERSION: 09.35.00
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -0,0 +1,48 @@
{
"_template": "Copy this file to scripts/sftp-config/sftp-config.demo.json — it is gitignored",
"_env": "demo",
"type": "sftp",
"save_before_upload": false,
"upload_on_save": false,
"sync_down_on_open": false,
"sync_skip_deletes": false,
"sync_same_age": true,
"confirm_downloads": false,
"confirm_sync": true,
"confirm_overwrite_newer": true,
"host": "YOUR_DEMO_HOST",
"user": "YOUR_DEMO_USERNAME",
"ssh_key_file": "jmiller_private.ppk",
"port": "22",
"remote_path": "/home/YOUR_USER/YOUR_DEMO_DOMAIN/htdocs/custom/YOUR_MODULE/",
"ignore_regexes": [
"\\.sublime-(project|workspace|settings)",
"\\.libsass.json/",
"sftp-config(-alt\\d?)?\\.json",
"sftp-settings\\.json",
"/venv/",
"\\.svn/",
"\\.hg/",
"\\.bzr",
"_darcs",
"CVS",
"\\.DS_Store",
"Thumbs\\.db",
"robots\\.txt",
"desktop\\.ini",
"configuration\\.php",
"\\.ffs*",
"\\.git*",
"\\.editorconfig",
"conf\\.php",
"\\.ps1",
"\\.tx"
],
"connect_timeout": 30
}
@@ -0,0 +1,49 @@
{
"_template": "Copy this file to scripts/sftp-config/sftp-config.live.json — it is gitignored",
"_env": "live",
"_note": "For multi-instance live deploy, use the LIVE_TARGETS env var instead (JSON array of target objects)",
"type": "sftp",
"save_before_upload": false,
"upload_on_save": false,
"sync_down_on_open": false,
"sync_skip_deletes": false,
"sync_same_age": true,
"confirm_downloads": false,
"confirm_sync": true,
"confirm_overwrite_newer": true,
"host": "YOUR_LIVE_HOST",
"user": "YOUR_LIVE_USERNAME",
"ssh_key_file": "~/.ssh/id_rsa",
"port": "22",
"remote_path": "/home/YOUR_USER/YOUR_LIVE_DOMAIN/htdocs/custom/YOUR_MODULE/",
"ignore_regexes": [
"\\.sublime-(project|workspace|settings)",
"\\.libsass.json/",
"sftp-config(-alt\\d?)?\\.json",
"sftp-settings\\.json",
"/venv/",
"\\.svn/",
"\\.hg/",
"\\.bzr",
"_darcs",
"CVS",
"\\.DS_Store",
"Thumbs\\.db",
"robots\\.txt",
"desktop\\.ini",
"configuration\\.php",
"\\.ffs*",
"\\.git*",
"\\.editorconfig",
"conf\\.php",
"\\.ps1",
"\\.tx"
],
"connect_timeout": 30
}
+1 -1
View File
@@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase
{
file_put_contents(
"{$this->tmpDir}/README.md",
"<!-- VERSION: 09.32.00 -->\nSome content\n"
"<!-- VERSION: 09.35.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.32.00 -->\n"
"# Test\n<!-- VERSION: 09.35.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.32.00 -->\n"
"<!-- VERSION: 09.35.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.32.00
* VERSION: 09.35.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;
}