b3d9ee8255
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
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 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
Wrap all CLI tools in cli/, automation/, maintenance/, deploy/, and release/ in classes extending CliFramework. Replaces manual $argv parsing with configure()/addArgument(), moves logic into run(): int, and converts fwrite(STDERR,...) to $this->log(). Two CLIApp subclasses (generate_dolibarr_version_txt, generate_joomla_update_xml) converted to extend CliFramework directly. Every script now gets free --help, --verbose, --quiet, --dry-run, --json, --no-color, banners, coloured logging, and progress bars. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
568 lines
18 KiB
PHP
568 lines
18 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: MokoPlatform.Scripts.Deploy
|
|
* INGROUP: MokoPlatform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /deploy/deploy-joomla.php
|
|
* BRIEF: Smart Joomla deploy — routes files to correct Joomla directories based on XML manifest
|
|
*
|
|
* Parses the extension's XML manifest to determine type (component, module,
|
|
* plugin, template, library, package) and deploys each section directly to
|
|
* its correct location on the Joomla server. No reinstall needed for code
|
|
* changes — only XML manifest changes require a Joomla reinstall.
|
|
*
|
|
* USAGE
|
|
* php deploy/deploy-joomla.php --path . --config /tmp/sftp-config.json
|
|
* php deploy/deploy-joomla.php --path . --config /tmp/sftp-config.json --dry-run
|
|
* php deploy/deploy-joomla.php --path . --env dev
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
use phpseclib3\Net\SFTP;
|
|
use phpseclib3\Crypt\PublicKeyLoader;
|
|
|
|
/**
|
|
* Joomla-aware SFTP deployment.
|
|
*
|
|
* Reads the extension XML manifest to determine:
|
|
* - Extension type (component, module, plugin, template, library)
|
|
* - Element name (com_xxx, mod_xxx, plg_group_name, tpl_xxx)
|
|
* - Client (site or administrator)
|
|
* - Plugin group (system, content, etc.)
|
|
*
|
|
* Then maps src/ subdirectories to their correct Joomla server paths:
|
|
*
|
|
* Component:
|
|
* src/admin/ -> administrator/components/{element}/
|
|
* src/site/ -> components/{element}/
|
|
* src/media/ -> media/{element}/
|
|
* src/api/ -> api/components/{element}/
|
|
*
|
|
* Module:
|
|
* src/ -> modules/{element}/ (site) or administrator/modules/{element}/ (admin)
|
|
* src/media/ -> media/{element}/
|
|
*
|
|
* Plugin:
|
|
* src/ -> plugins/{group}/{name}/
|
|
* src/media/ -> media/{element}/
|
|
*
|
|
* Template:
|
|
* src/ -> templates/{name}/ (site) or administrator/templates/{name}/ (admin)
|
|
* src/media/ -> media/templates/site/{name}/ or media/templates/administrator/{name}/
|
|
*
|
|
* Library:
|
|
* src/ -> libraries/{name}/
|
|
* src/media/ -> media/{element}/
|
|
*/
|
|
class DeployJoomlaCli extends CliFramework
|
|
{
|
|
private string $repoPath = '.';
|
|
private string $srcDir = 'src';
|
|
private array $config = [];
|
|
private int $uploaded = 0;
|
|
private int $unchanged = 0;
|
|
private int $skipped = 0;
|
|
private int $deleted = 0;
|
|
private array $ignorePatterns = [];
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Smart Joomla deploy — routes files to correct Joomla directories based on XML manifest');
|
|
$this->addArgument('--path', 'Repository root path', '.');
|
|
$this->addArgument('--src-dir', 'Source directory relative to path', 'src');
|
|
$this->addArgument('--config', 'Path to sftp-config.json', '');
|
|
$this->addArgument('--key-passphrase', 'SSH key passphrase', '');
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$this->repoPath = $this->getArgument('--path');
|
|
$this->srcDir = $this->getArgument('--src-dir');
|
|
$configPath = $this->getArgument('--config');
|
|
$keyPassphrase = $this->getArgument('--key-passphrase');
|
|
|
|
if ($keyPassphrase !== '') {
|
|
$this->config['key_passphrase'] = $keyPassphrase;
|
|
}
|
|
|
|
$this->repoPath = realpath($this->repoPath) ?: $this->repoPath;
|
|
|
|
// Resolve src dir
|
|
if (!str_starts_with($this->srcDir, '/')) {
|
|
$this->srcDir = "{$this->repoPath}/{$this->srcDir}";
|
|
}
|
|
// Try htdocs/ as fallback
|
|
if (!is_dir($this->srcDir) && is_dir("{$this->repoPath}/htdocs")) {
|
|
$this->srcDir = "{$this->repoPath}/htdocs";
|
|
}
|
|
|
|
// Load config
|
|
if ($configPath !== '' && file_exists($configPath)) {
|
|
$json = file_get_contents($configPath);
|
|
$json = preg_replace('#^\s*//.*$#m', '', $json);
|
|
$json = preg_replace('#,\s*([\]}])#', '$1', $json);
|
|
$parsed = json_decode($json, true);
|
|
if (is_array($parsed)) {
|
|
$this->config = array_merge($this->config, $parsed);
|
|
}
|
|
}
|
|
|
|
$manifest = $this->findManifest();
|
|
if ($manifest === null) {
|
|
$this->log('ERROR', "No Joomla XML manifest found in {$this->srcDir}");
|
|
return 1;
|
|
}
|
|
|
|
$ext = $this->parseManifest($manifest);
|
|
if ($ext === null) {
|
|
$this->log('ERROR', "Failed to parse manifest: {$manifest}");
|
|
return 1;
|
|
}
|
|
|
|
$this->log('INFO', "Extension: {$ext['type']} / {$ext['element']} (client: {$ext['client']})");
|
|
|
|
$deployMap = $this->buildDeployMap($ext);
|
|
if (empty($deployMap)) {
|
|
$this->log('ERROR', "No deploy mappings for extension type: {$ext['type']}");
|
|
return 1;
|
|
}
|
|
|
|
$this->log('INFO', "Deploy mappings:");
|
|
foreach ($deployMap as $map) {
|
|
$this->log('INFO', " {$map['local']} -> {$map['remote']}");
|
|
}
|
|
|
|
// Load ignore patterns
|
|
$this->ignorePatterns = $this->loadFtpIgnore();
|
|
|
|
// Check if manifest changed (warn user about reinstall)
|
|
$this->checkManifestChange($ext, $manifest);
|
|
|
|
if ($this->dryRun) {
|
|
$this->log('INFO', "[DRY RUN] Would deploy " . count($deployMap) . " mappings");
|
|
foreach ($deployMap as $map) {
|
|
if (is_dir($map['local'])) {
|
|
$count = iterator_count(
|
|
new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($map['local'], \FilesystemIterator::SKIP_DOTS)
|
|
)
|
|
);
|
|
$this->log('INFO', " {$map['local']} ({$count} files) -> {$map['remote']}");
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Connect
|
|
$sftp = $this->connectSftp();
|
|
if ($sftp === null) {
|
|
return 1;
|
|
}
|
|
|
|
// Deploy each mapping
|
|
$errors = 0;
|
|
foreach ($deployMap as $map) {
|
|
if (!is_dir($map['local'])) {
|
|
if ($this->verbose) {
|
|
$this->log('INFO', " SKIP: {$map['local']} (directory not found)");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Ensure remote directory exists
|
|
$stat = @$sftp->stat($map['remote']);
|
|
if ($stat === false) {
|
|
$this->log('INFO', " MKDIR: {$map['remote']}");
|
|
$sftp->mkdir($map['remote'], -1, true);
|
|
}
|
|
|
|
$result = $this->uploadDirectory($sftp, $map['local'], $map['remote']);
|
|
if ($result !== 0) {
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
// Also deploy the manifest file itself to the admin component directory
|
|
if ($ext['type'] === 'component' && file_exists($manifest)) {
|
|
$adminRemote = $this->getRemotePath($ext, 'admin');
|
|
$manifestName = basename($manifest);
|
|
$remoteDest = "{$adminRemote}/{$manifestName}";
|
|
$this->uploadFile($sftp, $manifest, $remoteDest);
|
|
$this->log('INFO', " Manifest: {$manifestName} -> {$remoteDest}");
|
|
}
|
|
|
|
$this->log('INFO', "Done. Uploaded: {$this->uploaded}, Unchanged: {$this->unchanged}, Skipped: {$this->skipped}");
|
|
|
|
return $errors > 0 ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Find the Joomla XML manifest file.
|
|
*/
|
|
private function findManifest(): ?string
|
|
{
|
|
$searchDirs = [$this->srcDir, $this->repoPath];
|
|
foreach ($searchDirs as $dir) {
|
|
$iterator = new \DirectoryIterator($dir);
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile() && $file->getExtension() === 'xml') {
|
|
$content = file_get_contents($file->getPathname());
|
|
if ($content !== false && str_contains($content, '<extension')) {
|
|
return $file->getPathname();
|
|
}
|
|
}
|
|
}
|
|
// Also check one level deep
|
|
foreach (new \DirectoryIterator($dir) as $subdir) {
|
|
if ($subdir->isDir() && !$subdir->isDot()) {
|
|
foreach (new \DirectoryIterator($subdir->getPathname()) as $file) {
|
|
if ($file->isFile() && $file->getExtension() === 'xml') {
|
|
$content = file_get_contents($file->getPathname());
|
|
if ($content !== false && str_contains($content, '<extension')) {
|
|
return $file->getPathname();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parse extension metadata from the XML manifest.
|
|
*
|
|
* @return array{type: string, element: string, client: string, group: string, name: string}|null
|
|
*/
|
|
private function parseManifest(string $path): ?array
|
|
{
|
|
$xml = @simplexml_load_file($path);
|
|
if ($xml === false || $xml->getName() !== 'extension') {
|
|
return null;
|
|
}
|
|
|
|
$type = (string) ($xml['type'] ?? 'component');
|
|
$client = (string) ($xml['client'] ?? 'site');
|
|
$group = (string) ($xml['group'] ?? '');
|
|
$element = (string) ($xml->element ?? '');
|
|
$name = (string) ($xml->name ?? '');
|
|
|
|
// Derive element from type + name if not explicit
|
|
if (empty($element)) {
|
|
$cleanName = strtolower(preg_replace('/[^a-zA-Z0-9_]/', '', $name));
|
|
$element = match ($type) {
|
|
'component' => "com_{$cleanName}",
|
|
'module' => "mod_{$cleanName}",
|
|
'plugin' => "plg_{$group}_{$cleanName}",
|
|
'template' => "tpl_{$cleanName}",
|
|
'library' => "lib_{$cleanName}",
|
|
default => $cleanName,
|
|
};
|
|
}
|
|
|
|
// For plugins, derive the short name (without plg_group_ prefix)
|
|
$shortName = $element;
|
|
if ($type === 'plugin' && preg_match('/^plg_\w+_(.+)$/', $element, $m)) {
|
|
$shortName = $m[1];
|
|
} elseif ($type === 'template' && preg_match('/^tpl_(.+)$/', $element, $m)) {
|
|
$shortName = $m[1];
|
|
}
|
|
|
|
return [
|
|
'type' => $type,
|
|
'element' => $element,
|
|
'client' => $client,
|
|
'group' => $group,
|
|
'name' => $name,
|
|
'shortName' => $shortName,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Build the local->remote deploy mapping based on extension type.
|
|
*
|
|
* @return array<int, array{local: string, remote: string}>
|
|
*/
|
|
private function buildDeployMap(array $ext): array
|
|
{
|
|
$remotePath = rtrim((string) $this->config['remote_path'], '/');
|
|
$src = $this->srcDir;
|
|
$map = [];
|
|
|
|
switch ($ext['type']) {
|
|
case 'component':
|
|
// Admin files
|
|
if (is_dir("{$src}/admin") || is_dir("{$src}/administrator")) {
|
|
$adminLocal = is_dir("{$src}/admin") ? "{$src}/admin" : "{$src}/administrator";
|
|
$map[] = ['local' => $adminLocal, 'remote' => "{$remotePath}/administrator/components/{$ext['element']}"];
|
|
}
|
|
// Site files
|
|
if (is_dir("{$src}/site")) {
|
|
$map[] = ['local' => "{$src}/site", 'remote' => "{$remotePath}/components/{$ext['element']}"];
|
|
}
|
|
// Media files
|
|
if (is_dir("{$src}/media")) {
|
|
$map[] = ['local' => "{$src}/media", 'remote' => "{$remotePath}/media/{$ext['element']}"];
|
|
}
|
|
// API files (Joomla 4+)
|
|
if (is_dir("{$src}/api")) {
|
|
$map[] = ['local' => "{$src}/api", 'remote' => "{$remotePath}/api/components/{$ext['element']}"];
|
|
}
|
|
// Language files (admin)
|
|
if (is_dir("{$src}/language/admin") || is_dir("{$src}/admin/language")) {
|
|
$langDir = is_dir("{$src}/language/admin") ? "{$src}/language/admin" : "{$src}/admin/language";
|
|
$map[] = ['local' => $langDir, 'remote' => "{$remotePath}/administrator/language"];
|
|
}
|
|
// Language files (site)
|
|
if (is_dir("{$src}/language/site") || is_dir("{$src}/site/language")) {
|
|
$langDir = is_dir("{$src}/language/site") ? "{$src}/language/site" : "{$src}/site/language";
|
|
$map[] = ['local' => $langDir, 'remote' => "{$remotePath}/language"];
|
|
}
|
|
break;
|
|
|
|
case 'module':
|
|
$base = $ext['client'] === 'administrator'
|
|
? "{$remotePath}/administrator/modules/{$ext['element']}"
|
|
: "{$remotePath}/modules/{$ext['element']}";
|
|
$map[] = ['local' => $src, 'remote' => $base];
|
|
if (is_dir("{$src}/media")) {
|
|
$map[] = ['local' => "{$src}/media", 'remote' => "{$remotePath}/media/{$ext['element']}"];
|
|
}
|
|
break;
|
|
|
|
case 'plugin':
|
|
$map[] = ['local' => $src, 'remote' => "{$remotePath}/plugins/{$ext['group']}/{$ext['shortName']}"];
|
|
if (is_dir("{$src}/media")) {
|
|
$map[] = ['local' => "{$src}/media", 'remote' => "{$remotePath}/media/{$ext['element']}"];
|
|
}
|
|
break;
|
|
|
|
case 'template':
|
|
$clientDir = $ext['client'] === 'administrator' ? 'administrator/' : '';
|
|
$map[] = ['local' => $src, 'remote' => "{$remotePath}/{$clientDir}templates/{$ext['shortName']}"];
|
|
if (is_dir("{$src}/media")) {
|
|
$mediaClient = $ext['client'] === 'administrator' ? 'administrator' : 'site';
|
|
$map[] = ['local' => "{$src}/media", 'remote' => "{$remotePath}/media/templates/{$mediaClient}/{$ext['shortName']}"];
|
|
}
|
|
break;
|
|
|
|
case 'library':
|
|
$map[] = ['local' => $src, 'remote' => "{$remotePath}/libraries/{$ext['shortName']}"];
|
|
if (is_dir("{$src}/media")) {
|
|
$map[] = ['local' => "{$src}/media", 'remote' => "{$remotePath}/media/{$ext['element']}"];
|
|
}
|
|
break;
|
|
|
|
case 'package':
|
|
// Packages deploy their sub-extensions individually
|
|
// For now, deploy to administrator/manifests/packages/
|
|
$map[] = ['local' => $src, 'remote' => "{$remotePath}/administrator/manifests/packages"];
|
|
break;
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
/**
|
|
* Get the remote path for a specific section of the extension.
|
|
*/
|
|
private function getRemotePath(array $ext, string $section): string
|
|
{
|
|
$remotePath = rtrim((string) $this->config['remote_path'], '/');
|
|
return match ($section) {
|
|
'admin' => "{$remotePath}/administrator/components/{$ext['element']}",
|
|
'site' => "{$remotePath}/components/{$ext['element']}",
|
|
'media' => "{$remotePath}/media/{$ext['element']}",
|
|
default => $remotePath,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if the XML manifest has changed and warn about reinstall.
|
|
*/
|
|
private function checkManifestChange(array $ext, string $manifestPath): void
|
|
{
|
|
$manifestName = basename($manifestPath);
|
|
$this->log('INFO', '');
|
|
$this->log('INFO', "NOTE: If {$manifestName} has changed (new fields, permissions, menu items,");
|
|
$this->log('INFO', ' database schema), you must reinstall the extension through Joomla.');
|
|
$this->log('INFO', ' Code changes (PHP, JS, CSS, language) do NOT require reinstall.');
|
|
$this->log('INFO', '');
|
|
}
|
|
|
|
/**
|
|
* Upload a directory recursively to the remote server.
|
|
*/
|
|
private function uploadDirectory(SFTP $sftp, string $localDir, string $remoteDir): int
|
|
{
|
|
$errors = 0;
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($localDir, \FilesystemIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $item) {
|
|
$relativePath = substr($item->getPathname(), strlen($localDir) + 1);
|
|
$relativePath = str_replace('\\', '/', $relativePath);
|
|
$remotePath = "{$remoteDir}/{$relativePath}";
|
|
|
|
// Check ignore patterns
|
|
if ($this->shouldIgnore($relativePath)) {
|
|
$this->skipped++;
|
|
continue;
|
|
}
|
|
|
|
if ($item->isDir()) {
|
|
$stat = @$sftp->stat($remotePath);
|
|
if ($stat === false) {
|
|
$sftp->mkdir($remotePath, -1, true);
|
|
}
|
|
} else {
|
|
$result = $this->uploadFile($sftp, $item->getPathname(), $remotePath);
|
|
if (!$result) {
|
|
$errors++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Upload a single file with smart diff (skip if unchanged).
|
|
*/
|
|
private function uploadFile(SFTP $sftp, string $localPath, string $remotePath): bool
|
|
{
|
|
$localSize = filesize($localPath);
|
|
$remoteStat = @$sftp->stat($remotePath);
|
|
|
|
// Smart diff: skip if same size and hash
|
|
if ($remoteStat !== false && ($remoteStat['size'] ?? -1) === $localSize) {
|
|
$remoteContent = @$sftp->get($remotePath);
|
|
if ($remoteContent !== false && md5($remoteContent) === md5_file($localPath)) {
|
|
$this->unchanged++;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Ensure parent directory exists
|
|
$parentDir = dirname($remotePath);
|
|
$parentStat = @$sftp->stat($parentDir);
|
|
if ($parentStat === false) {
|
|
$sftp->mkdir($parentDir, -1, true);
|
|
}
|
|
|
|
$result = $sftp->put($remotePath, $localPath, SFTP::SOURCE_LOCAL_FILE);
|
|
if ($result) {
|
|
$this->uploaded++;
|
|
if ($this->verbose) {
|
|
$this->log('INFO', " UPLOAD: {$remotePath}");
|
|
}
|
|
} else {
|
|
$this->log('ERROR', " FAIL: {$remotePath}");
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check if a relative path should be ignored.
|
|
*/
|
|
private function shouldIgnore(string $relativePath): bool
|
|
{
|
|
foreach ($this->ignorePatterns as $pattern) {
|
|
if (preg_match($pattern, $relativePath)) {
|
|
return true;
|
|
}
|
|
}
|
|
// Always skip dotfiles and common non-deploy files
|
|
$basename = basename($relativePath);
|
|
if (str_starts_with($basename, '.') && $basename !== '.htaccess') {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Load .ftpignore patterns.
|
|
*
|
|
* @return string[] Regex patterns
|
|
*/
|
|
private function loadFtpIgnore(): array
|
|
{
|
|
$patterns = [];
|
|
foreach ([$this->srcDir, $this->repoPath] as $dir) {
|
|
$file = "{$dir}/.ftpignore";
|
|
if (!file_exists($file)) {
|
|
continue;
|
|
}
|
|
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || str_starts_with($line, '#')) {
|
|
continue;
|
|
}
|
|
// Convert glob to regex
|
|
$regex = str_replace(['.', '*', '?'], ['\\.', '.*', '.'], $line);
|
|
$patterns[] = "#^{$regex}(/|$)#i";
|
|
}
|
|
}
|
|
return $patterns;
|
|
}
|
|
|
|
/**
|
|
* Connect to the SFTP server.
|
|
*/
|
|
private function connectSftp(): ?SFTP
|
|
{
|
|
$host = (string) $this->config['host'];
|
|
$port = (int) ($this->config['port'] ?? 22);
|
|
$user = (string) $this->config['user'];
|
|
|
|
$this->log('INFO', "Connecting to {$user}@{$host}:{$port}...");
|
|
|
|
$sftp = new SFTP($host, $port, 30);
|
|
|
|
// Try key auth first
|
|
if (!empty($this->config['ssh_key_file'])) {
|
|
$keyPath = $this->config['ssh_key_file'];
|
|
if (!file_exists($keyPath)) {
|
|
$keyPath = "{$this->repoPath}/scripts/keys/{$keyPath}";
|
|
}
|
|
if (file_exists($keyPath)) {
|
|
$passphrase = $this->config['key_passphrase'] ?? '';
|
|
$key = PublicKeyLoader::load(file_get_contents($keyPath), $passphrase);
|
|
if ($sftp->login($user, $key)) {
|
|
$this->log('INFO', 'Connected via SSH key');
|
|
return $sftp;
|
|
}
|
|
$this->warning('Key auth failed');
|
|
}
|
|
}
|
|
|
|
// Fallback to password
|
|
if (!empty($this->config['password'])) {
|
|
if ($sftp->login($user, $this->config['password'])) {
|
|
$this->log('INFO', 'Connected via password');
|
|
return $sftp;
|
|
}
|
|
}
|
|
|
|
$this->log('ERROR', 'Authentication failed');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$app = new DeployJoomlaCli();
|
|
exit($app->execute());
|