Public Access
Compare commits
28 Commits
development
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 00f0e44c78 | |||
| c976f400f4 | |||
| ebf37423f2 | |||
| e4d836067f | |||
| 8900b12f81 | |||
| 4fc3d0a4a9 | |||
| 19aa0111f0 | |||
| 46e33a9383 | |||
| 2f43bac247 | |||
| 817e9caee8 | |||
| 6216803590 | |||
| caad8ee7d0 | |||
| 558c0a0edf | |||
| cb1053274e | |||
| 743da9c4c2 | |||
| 4b6fcb5fa4 | |||
| e7b2c1fba2 | |||
| 2a45dd873b | |||
| e0f1ec1372 | |||
| f325de91d4 | |||
| 109493ab4a | |||
| 0d3b14d55c | |||
| 35075aa743 | |||
| 7be017ae30 | |||
| e8a3414ff4 | |||
| e8697c2d0e | |||
| 7d369628f0 | |||
| e834b8a3ea |
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 09.32.01
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
+6
-24
@@ -12,32 +12,14 @@ BRIEF: Release changelog
|
|||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [09.32.00] --- 2026-06-21
|
## [09.36.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.32.00] --- 2026-06-21
|
## [09.36.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.31.00] --- 2026-06-21
|
## [09.35.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.31.00] --- 2026-06-21
|
## [09.35.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.30.00] --- 2026-06-21
|
## [09.34.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.30.00] --- 2026-06-21
|
## [09.34.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/`
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root
|
|||||||
INGROUP: MokoPlatform
|
INGROUP: MokoPlatform
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
PATH: /README.md
|
PATH: /README.md
|
||||||
VERSION: 09.32.01
|
VERSION: 09.36.00
|
||||||
BRIEF: Project overview and documentation
|
BRIEF: Project overview and documentation
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -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.36.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());
|
||||||
@@ -89,6 +89,7 @@ const COMMAND_MAP = [
|
|||||||
|
|
||||||
// Automation
|
// Automation
|
||||||
'sync' => 'automation/bulk_sync.php',
|
'sync' => 'automation/bulk_sync.php',
|
||||||
|
'deps:update' => 'automation/update_dependencies.php',
|
||||||
'automation:cleanup' => 'automation/repo_cleanup.php',
|
'automation:cleanup' => 'automation/repo_cleanup.php',
|
||||||
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
|
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
|
||||||
|
|
||||||
@@ -198,6 +199,7 @@ const COMMAND_MAP = [
|
|||||||
'deploy:sftp' => 'deploy/deploy-sftp.php',
|
'deploy:sftp' => 'deploy/deploy-sftp.php',
|
||||||
'deploy:backup' => 'deploy/backup-before-deploy.php',
|
'deploy:backup' => 'deploy/backup-before-deploy.php',
|
||||||
'deploy:health-check' => 'deploy/health-check.php',
|
'deploy:health-check' => 'deploy/health-check.php',
|
||||||
|
'deploy:verify' => 'deploy/deploy-and-verify.php',
|
||||||
'deploy:rollback' => 'deploy/rollback-joomla.php',
|
'deploy:rollback' => 'deploy/rollback-joomla.php',
|
||||||
'deploy:sync' => 'deploy/sync-joomla.php',
|
'deploy:sync' => 'deploy/sync-joomla.php',
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/branch_rename.php
|
* PATH: /cli/branch_rename.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/bulk_workflow_push.php
|
* PATH: /cli/bulk_workflow_push.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/bulk_workflow_trigger.php
|
* PATH: /cli/bulk_workflow_trigger.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Trigger a workflow across multiple repos at once
|
* BRIEF: Trigger a workflow across multiple repos at once
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/client_dashboard.php
|
* PATH: /cli/client_dashboard.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Generate unified client dashboard HTML
|
* BRIEF: Generate unified client dashboard HTML
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/client_inventory.php
|
* PATH: /cli/client_inventory.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/client_provision.php
|
* PATH: /cli/client_provision.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Provision a new client environment end-to-end
|
* BRIEF: Provision a new client environment end-to-end
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/grafana_dashboard.php
|
* PATH: /cli/grafana_dashboard.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Manage Grafana dashboards via API
|
* BRIEF: Manage Grafana dashboards via API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/joomla_build.php
|
* PATH: /cli/joomla_build.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
||||||
* NOTE: Called by pre-release and auto-release workflows.
|
* NOTE: Called by pre-release and auto-release workflows.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/joomla_metadata_validate.php
|
* PATH: /cli/joomla_metadata_validate.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
|
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/manifest_detect.php
|
* PATH: /cli/manifest_detect.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/manifest_integrity.php
|
* PATH: /cli/manifest_integrity.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Cross-check manifest API fields against repo contents across the org
|
* BRIEF: Cross-check manifest API fields against repo contents across the org
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/manifest_licensing.php
|
* PATH: /cli/manifest_licensing.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokocli
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/manifest_read.php
|
* PATH: /cli/manifest_read.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
|
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/platform_detect.php
|
* PATH: /cli/platform_detect.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Auto-detect repository platform type and optionally update manifest
|
* BRIEF: Auto-detect repository platform type and optionally update manifest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokocli
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_cascade.php
|
* PATH: /cli/release_cascade.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Cascade release zip to all lower stability channels
|
* BRIEF: Cascade release zip to all lower stability channels
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/release_publish.php
|
* PATH: /cli/release_publish.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/scaffold_client.php
|
* PATH: /cli/scaffold_client.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/updates_xml_sync.php
|
* PATH: /cli/updates_xml_sync.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Sync updates.xml to target branches via Gitea API
|
* BRIEF: Sync updates.xml to target branches via Gitea API
|
||||||
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
||||||
* is modified on the current branch. Pushes the file to other branches
|
* is modified on the current branch. Pushes the file to other branches
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/version_auto_bump.php
|
* PATH: /cli/version_auto_bump.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ class VersionBumpCli extends CliFramework
|
|||||||
/**
|
/**
|
||||||
* Scan git release tags for the highest version across all channels.
|
* Scan git release tags for the highest version across all channels.
|
||||||
*
|
*
|
||||||
* Checks release names like "MokoSuiteClient (VERSION: 09.32.01)" in
|
* Checks release names like "MokoSuiteClient (VERSION: 09.36.00)" in
|
||||||
* git tags (stable, release-candidate, development, etc.) to find the
|
* git tags (stable, release-candidate, development, etc.) to find the
|
||||||
* highest version that has been released on any channel.
|
* highest version that has been released on any channel.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/version_check.php
|
* PATH: /cli/version_check.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Validate version consistency across README, manifests, and sub-packages
|
* BRIEF: Validate version consistency across README, manifests, and sub-packages
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/wiki_sync.php
|
* PATH: /cli/wiki_sync.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Sync select wiki pages from mokoplatform to all template repos
|
* BRIEF: Sync select wiki pages from mokoplatform to all template repos
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: moko-platform
|
* INGROUP: moko-platform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
* PATH: /cli/workflow_sync.php
|
* PATH: /cli/workflow_sync.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
|
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/backup-before-deploy.php
|
* PATH: /deploy/backup-before-deploy.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
|
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/deploy-dolibarr.php
|
* PATH: /deploy/deploy-dolibarr.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
|
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+111
-4
@@ -66,8 +66,16 @@ class DeploySftp extends CliFramework
|
|||||||
*/
|
*/
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
{
|
{
|
||||||
$repoPath = $this->resolveRepoPath();
|
$repoPath = $this->resolveRepoPath();
|
||||||
$srcDir = $this->resolveSrcDir($repoPath);
|
$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);
|
$configPath = $this->resolveConfigPath($repoPath);
|
||||||
|
|
||||||
$this->log("Repository : {$repoPath}");
|
$this->log("Repository : {$repoPath}");
|
||||||
@@ -130,6 +138,103 @@ class DeploySftp extends CliFramework
|
|||||||
return $exitCode;
|
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 ──────────────────────────────────────────────────────
|
// ─── Private helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,8 +276,10 @@ class DeploySftp extends CliFramework
|
|||||||
|
|
||||||
/** Map of --env values to their sftp-config filename. */
|
/** Map of --env values to their sftp-config filename. */
|
||||||
private const ENV_CONFIG_MAP = [
|
private const ENV_CONFIG_MAP = [
|
||||||
'dev' => 'sftp-config.dev.json',
|
'dev' => 'sftp-config.dev.json',
|
||||||
'rs' => 'sftp-config.rs.json',
|
'rs' => 'sftp-config.rs.json',
|
||||||
|
'demo' => 'sftp-config.demo.json',
|
||||||
|
'live' => 'sftp-config.live.json',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/health-check.php
|
* PATH: /deploy/health-check.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Post-deploy health check — verify a Joomla site is responding correctly
|
* BRIEF: Post-deploy health check — verify a Joomla site is responding correctly
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/rollback-joomla.php
|
* PATH: /deploy/rollback-joomla.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot
|
* BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/sync-joomla.php
|
* PATH: /deploy/sync-joomla.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Sync Joomla site directories between two servers via rsync over SSH
|
* BRIEF: Sync Joomla site directories between two servers via rsync over SSH
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -590,6 +590,25 @@ abstract class CliFramework
|
|||||||
$this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n");
|
$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
|
// Console graphics — progress bar
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
DEFGROUP: dolibarr-api-mcp.Documentation
|
DEFGROUP: dolibarr-api-mcp.Documentation
|
||||||
INGROUP: dolibarr-api-mcp
|
INGROUP: dolibarr-api-mcp
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
VERSION: 09.32.01
|
VERSION: 09.36.00
|
||||||
PATH: ./CONTRIBUTING.md
|
PATH: ./CONTRIBUTING.md
|
||||||
BRIEF: Contribution guidelines for the project
|
BRIEF: Contribution guidelines for the project
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ DEFGROUP: dolibarr-api-mcp.Documentation
|
|||||||
INGROUP: dolibarr-api-mcp
|
INGROUP: dolibarr-api-mcp
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 09.32.01
|
VERSION: 09.36.00
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
DEFGROUP:
|
DEFGROUP:
|
||||||
INGROUP: Project.Documentation
|
INGROUP: Project.Documentation
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli-Template-Generic
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli-Template-Generic
|
||||||
VERSION: 09.32.01
|
VERSION: 09.36.00
|
||||||
PATH: ./CONTRIBUTING.md
|
PATH: ./CONTRIBUTING.md
|
||||||
BRIEF: Contribution guidelines for the project
|
BRIEF: Contribution guidelines for the project
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
|||||||
INGROUP: [PROJECT_NAME].Documentation
|
INGROUP: [PROJECT_NAME].Documentation
|
||||||
REPO: [REPOSITORY_URL]
|
REPO: [REPOSITORY_URL]
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 09.32.01
|
VERSION: 09.36.00
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
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
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase
|
|||||||
{
|
{
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
"{$this->tmpDir}/README.md",
|
"{$this->tmpDir}/README.md",
|
||||||
"<!-- VERSION: 09.32.01 -->\nSome content\n"
|
"<!-- VERSION: 09.36.00 -->\nSome content\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->execute();
|
$this->execute();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class VersionReadTest extends TestCase
|
|||||||
{
|
{
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
"{$this->tmpDir}/README.md",
|
"{$this->tmpDir}/README.md",
|
||||||
"# Test\n<!-- VERSION: 09.32.01 -->\n"
|
"# Test\n<!-- VERSION: 09.36.00 -->\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertSame('02.03.04', trim($this->runScript()));
|
$this->assertSame('02.03.04', trim($this->runScript()));
|
||||||
@@ -68,7 +68,7 @@ class VersionReadTest extends TestCase
|
|||||||
{
|
{
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
"{$this->tmpDir}/README.md",
|
"{$this->tmpDir}/README.md",
|
||||||
"<!-- VERSION: 09.32.01 -->\n"
|
"<!-- VERSION: 09.36.00 -->\n"
|
||||||
);
|
);
|
||||||
mkdir("{$this->tmpDir}/src", 0755, true);
|
mkdir("{$this->tmpDir}/src", 0755, true);
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /validate/check_file_integrity.php
|
* PATH: /validate/check_file_integrity.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.36.00
|
||||||
* BRIEF: Compare deployed files on a remote server against the local repository to detect drift
|
* BRIEF: Compare deployed files on a remote server against the local repository to detect drift
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
use MokoCli\RecoverySuggestion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that the required directories and files exist in the repository root.
|
* 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)) {
|
if (!is_dir($path . '/' . $dir)) {
|
||||||
$missingDirs[] = $dir;
|
$missingDirs[] = $dir;
|
||||||
$this->status(false, "Directory: {$dir}");
|
$this->status(false, "Directory: {$dir}");
|
||||||
|
$this->suggest(RecoverySuggestion::forMissingDirectory($dir));
|
||||||
$failed++;
|
$failed++;
|
||||||
} else {
|
} else {
|
||||||
$this->status(true, "Directory: {$dir}");
|
$this->status(true, "Directory: {$dir}");
|
||||||
@@ -96,6 +98,7 @@ class CheckStructure extends CliFramework
|
|||||||
if (!is_file($path . '/' . $file)) {
|
if (!is_file($path . '/' . $file)) {
|
||||||
$missingFiles[] = $file;
|
$missingFiles[] = $file;
|
||||||
$this->status(false, "File: {$file}");
|
$this->status(false, "File: {$file}");
|
||||||
|
$this->suggest(RecoverySuggestion::forMissingFile($file));
|
||||||
$failed++;
|
$failed++;
|
||||||
} else {
|
} else {
|
||||||
$this->status(true, "File: {$file}");
|
$this->status(true, "File: {$file}");
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
use MokoCli\RecoverySuggestion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks that the version recorded in composer.json matches VERSION headers
|
* Checks that the version recorded in composer.json matches VERSION headers
|
||||||
@@ -101,6 +102,7 @@ class CheckVersionConsistency extends CliFramework
|
|||||||
if ($match[0] !== $expected) {
|
if ($match[0] !== $expected) {
|
||||||
$line = substr_count(substr($content, 0, (int) $match[1]), "\n") + 1;
|
$line = substr_count(substr($content, 0, (int) $match[1]), "\n") + 1;
|
||||||
$this->status(false, $filename, "line {$line}: found {$match[0]}, expected {$expected}");
|
$this->status(false, $filename, "line {$line}: found {$match[0]}, expected {$expected}");
|
||||||
|
$this->suggest(RecoverySuggestion::forVersionMismatch($filename, $match[0], $expected));
|
||||||
$issues[] = $filename;
|
$issues[] = $filename;
|
||||||
$filePassed = false;
|
$filePassed = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user