Files
Jonathan Miller 48d574e225
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 43s
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
feat: release_mirror.php — mirror Gitea releases to GitHub
New CLI tool that mirrors a Gitea release (with assets) to a GitHub
repository. Replaces the 40-line inline bash in auto-release.yml Step 9.

Supports create/update, asset download+upload, and proper GitHub API
headers (User-Agent, Accept).

Closes #160

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 14:46:59 -05:00

361 lines
14 KiB
PHP

#!/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: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /bin/moko
* BRIEF: Unified CLI dispatcher — run any MokoStandards script without needing GitHub Actions
*
* USAGE
* php bin/moko <command> [options] (all platforms)
* ./bin/moko <command> [options] (Unix, after: chmod +x bin/moko)
*
* COMMANDS
* sync Bulk-sync MokoStandards to organisation repos
* health Full repository health check (runs most validators)
* inventory Refresh docs/reference/REPOSITORY_INVENTORY.md
*
* check:syntax PHP syntax check (php -l) on all tracked .php files
* check:version Verify VERSION fields and badges match composer.json
* check:changelog Validate CHANGELOG.md format
* check:structure Verify required root files and directories
* check:headers Check SPDX-License-Identifier presence in source files
* check:secrets Scan for leaked credentials / API keys
* check:tabs Detect tab characters in YAML files
* check:paths Detect backslash path separators in PHP source
* check:xml Validate XML files are well-formed
* check:enterprise Full enterprise-readiness check (headers, strict types, PSR-12)
* check:dolibarr Validate Dolibarr module directory structure
* check:joomla Validate Joomla XML manifest
* check:language Validate Joomla/Dolibarr .ini language files
* detect Auto-detect repository platform type
* drift Scan org repos for drift from MokoStandards templates
*
* COMMON OPTIONS (passed through to each script)
* --path <dir> Repository root to check (default: .)
* --dry-run Preview changes without applying them
* --verbose Show passing checks as well as failures
* --quiet Show only failures
* --json Machine-readable JSON output
* --help Show help for the selected command
*
* AUTHENTICATION
* Token resolution order (first non-empty wins):
* 1. GH_TOKEN environment variable
* 2. GITHUB_TOKEN environment variable
* 3. `gh auth token` (GitHub CLI — run `gh auth login` once)
* 4. .env file in repo root (GH_TOKEN=... line)
*
* EXAMPLES
* php bin/moko health
* php bin/moko sync -- --repos MokoDoliTraining --dry-run
* php bin/moko check:version --path .
* php bin/moko drift -- --org mokoconsulting-tech --json
*/
declare(strict_types=1);
// ── Bootstrap ────────────────────────────────────────────────────────────────
$repoRoot = dirname(__DIR__);
$autoloader = $repoRoot . '/vendor/autoload.php';
// Support global Composer installs (e.g. composer global require)
if (isset($GLOBALS['_composer_autoload_path'])) {
$autoloader = $GLOBALS['_composer_autoload_path'];
}
if (!is_file($autoloader)) {
fwrite(STDERR, "Error: vendor/autoload.php not found.\nRun: composer install\n");
exit(2);
}
require_once $autoloader;
// ── Command map ──────────────────────────────────────────────────────────────
/**
* Map of moko command names → relative path to the PHP script.
* All paths are relative to the repo root.
*/
const COMMAND_MAP = [
// Automation
'sync' => 'automation/bulk_sync.php',
// Maintenance
'inventory' => 'maintenance/update_repo_inventory.php',
// Validation — general
'health' => 'validate/check_repo_health.php',
'check:syntax' => 'validate/check_php_syntax.php',
'check:version' => 'validate/check_version_consistency.php',
'check:changelog' => 'validate/check_changelog.php',
'check:structure' => 'validate/check_structure.php',
'check:headers' => 'validate/check_license_headers.php',
'check:secrets' => 'validate/check_no_secrets.php',
'check:tabs' => 'validate/check_tabs.php',
'check:paths' => 'validate/check_paths.php',
'check:xml' => 'validate/check_xml_wellformed.php',
'check:enterprise' => 'validate/check_enterprise_readiness.php',
// Validation — platform-specific
'check:dolibarr' => 'validate/check_dolibarr_module.php',
'check:joomla' => 'validate/check_joomla_manifest.php',
'check:language' => 'validate/check_language_structure.php',
'check:client' => 'validate/check_client_theme.php',
'check:wiki' => 'validate/check_wiki_health.php',
// Detection
'detect' => 'validate/auto_detect_platform.php',
// Org-wide
'drift' => 'validate/scan_drift.php',
// Release
'release' => 'cli/release.php',
'release:notes' => 'cli/release_notes.php',
'release:validate' => 'cli/release_validate.php',
'manifest:element' => 'cli/manifest_element.php',
'release:cascade' => 'cli/release_cascade.php',
'release:promote' => 'cli/release_promote.php',
'release:create' => 'cli/release_create.php',
'release:manage' => 'cli/release_manage.php',
'release:mirror' => 'cli/release_mirror.php',
'release:package' => 'cli/release_package.php',
// Version management
'version:read' => 'cli/version_read.php',
'version:bump' => 'cli/version_bump.php',
'version:check' => 'cli/version_check.php',
'version:propagate' => 'maintenance/update_version_from_readme.php',
'version:set-platform' => 'cli/version_set_platform.php',
'version:reset-dev' => 'cli/version_reset_dev.php',
// Build & package
'build:package' => 'cli/package_build.php',
'build:joomla' => 'cli/joomla_build.php',
'build:updates-xml' => 'cli/updates_xml_build.php',
// Platform detection
'platform:detect' => 'cli/platform_detect.php',
'manifest:read' => 'cli/manifest_read.php',
// Repository management
'repo:create' => 'cli/create_repo.php',
'repo:archive' => 'cli/archive_repo.php',
'repo:scaffold-client' => 'cli/scaffold_client.php',
'repo:provision' => 'cli/client_provision.php',
// Bulk operations
'bulk:push-workflow' => 'cli/bulk_workflow_push.php',
'bulk:trigger' => 'cli/bulk_workflow_trigger.php',
'bulk:sync-rulesets' => 'cli/sync_rulesets.php',
// Monitoring & dashboards
'dashboard' => 'cli/client_dashboard.php',
'grafana' => 'cli/grafana_dashboard.php',
'client:inventory' => 'cli/client_inventory.php',
// Module validation
'validate:module' => 'bin/validate-module',
];
// ── Argument parsing ─────────────────────────────────────────────────────────
$args = array_slice($argv, 1);
$command = array_shift($args) ?? '';
// Strip leading -- separator that Composer passes when using `composer run-script cmd -- extra-args`
if (isset($args[0]) && $args[0] === '--') {
array_shift($args);
}
// ── Help / list ───────────────────────────────────────────────────────────────
if ($command === '' || $command === '--help' || $command === '-h' || $command === 'help') {
printHelp();
exit(0);
}
if ($command === 'list' || $command === 'commands') {
printCommandList();
exit(0);
}
// ── Dispatch ──────────────────────────────────────────────────────────────────
if (!array_key_exists($command, COMMAND_MAP)) {
fwrite(STDERR, "Error: Unknown command '{$command}'\n\n");
printCommandList();
exit(2);
}
$scriptPath = $repoRoot . '/' . COMMAND_MAP[$command];
if (!is_file($scriptPath)) {
fwrite(STDERR, "Error: Script not found: " . COMMAND_MAP[$command] . "\n");
fwrite(STDERR, "Ensure the repository is complete and run: composer install\n");
exit(2);
}
// Rebuild $argv as if the target script were invoked directly, then include it.
// This is equivalent to: php <script> [args…] but keeps us in the same process.
$argv = array_merge([$scriptPath], $args);
$argc = count($argv);
// Suppress the "run directly" guard that some scripts use (they check realpath($argv[0]) === __FILE__).
// By setting $argv[0] to the script's own path the guard passes naturally.
require $scriptPath;
// ── Helpers ───────────────────────────────────────────────────────────────────
function printHelp(): void
{
echo <<<'HELP'
╔══════════════════════════════════════════════════════════╗
║ MokoStandards CLI (bin/moko) ║
╚══════════════════════════════════════════════════════════╝
Run any MokoStandards script locally without GitHub Actions.
USAGE
php bin/moko <command> [options] (all platforms)
./bin/moko <command> [options] (Unix, after: chmod +x bin/moko)
Run `php bin/moko list` to see all available commands.
Run `php bin/moko <command> --help` for command-specific help.
QUICK START
1. composer install
2. cp .env.example .env # add your GH_TOKEN
3. php bin/moko health # run full health check
AUTHENTICATION
GH_TOKEN env var → GITHUB_TOKEN env var → gh auth login
HELP;
}
function printCommandList(): void
{
echo "Available commands:\n\n";
// Auto-group by command prefix or comment-based sections
$groups = [];
foreach (COMMAND_MAP as $cmd => $path) {
if (str_contains($cmd, ':')) {
$prefix = explode(':', $cmd)[0];
$groupName = match ($prefix) {
'check' => 'Validation',
'version' => 'Version',
'release' => 'Release',
'build' => 'Build',
'platform', 'manifest' => 'Platform',
'repo' => 'Repository',
'bulk' => 'Bulk Operations',
'client' => 'Client Management',
'validate' => 'Module Validation',
default => ucfirst($prefix),
};
} else {
$groupName = match ($cmd) {
'sync' => 'Automation',
'inventory' => 'Maintenance',
'health' => 'Validation',
'detect', 'drift' => 'Validation',
'dashboard', 'grafana' => 'Monitoring',
default => 'Other',
};
}
$groups[$groupName][$cmd] = $path;
}
// Load plugin commands
$pluginCommands = loadPluginCommands();
if (!empty($pluginCommands)) {
foreach ($pluginCommands as $cmd => $info) {
$type = $info['plugin'] ?? 'Plugin';
$groups["Plugin: {$type}"][$cmd] = $info['description'] ?? '';
}
}
ksort($groups);
foreach ($groups as $group => $commands) {
echo " \033[1m{$group}\033[0m\n";
ksort($commands);
foreach ($commands as $cmd => $path) {
printf(" \033[36m%-26s\033[0m %s\n", $cmd, basename($path));
}
echo "\n";
}
$total = count(COMMAND_MAP) + count($pluginCommands);
echo "{$total} command(s) available.\n";
echo "Run: php bin/moko <command> --help\n";
}
/**
* Load commands from registered plugins.
*
* @return array<string, array{plugin: string, description: string, script: string}>
*/
function loadPluginCommands(): array
{
$pluginDir = dirname(__DIR__) . '/lib/Enterprise/Plugins';
if (!is_dir($pluginDir)) {
return [];
}
$commands = [];
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
$className = 'MokoEnterprise\\Plugins\\'
. pathinfo($file, PATHINFO_FILENAME);
if (!class_exists($className)) {
continue;
}
try {
$ref = new \ReflectionClass($className);
if ($ref->isAbstract()) {
continue;
}
$plugin = $ref->newInstanceWithoutConstructor();
$pluginCmds = $plugin->getCommands();
foreach ($pluginCmds as $cmd) {
$name = $cmd['name'] ?? '';
if ($name === '') {
continue;
}
$type = method_exists($plugin, 'getProjectType')
? $plugin->getProjectType() : 'unknown';
$commands[$name] = [
'plugin' => $type,
'description' => $cmd['description'] ?? '',
'script' => $cmd['script'] ?? '',
];
}
} catch (\Throwable $e) {
// Skip plugins that can't be instantiated
continue;
}
}
return $commands;
}