Public Access
feat: deploy-sftp.php supports --env demo and --env live with multi-instance (#184)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
- Add demo and live to ENV_CONFIG_MAP - Add multi-target deploy via LIVE_TARGETS env var (JSON array of targets) - Add sftp-config.demo.json.example and sftp-config.live.json.example templates - Failed targets logged but don't block remaining deploys
This commit is contained in:
+111
-4
@@ -66,8 +66,16 @@ class DeploySftp extends CliFramework
|
||||
*/
|
||||
protected function run(): int
|
||||
{
|
||||
$repoPath = $this->resolveRepoPath();
|
||||
$srcDir = $this->resolveSrcDir($repoPath);
|
||||
$repoPath = $this->resolveRepoPath();
|
||||
$srcDir = $this->resolveSrcDir($repoPath);
|
||||
$env = strtolower($this->getArgument('--env', '') ?: '');
|
||||
|
||||
// Multi-target: LIVE_TARGETS env var overrides config file for live deploys
|
||||
$liveTargets = getenv('LIVE_TARGETS') ?: '';
|
||||
if ($liveTargets !== '' && ($env === 'live' || $env === '')) {
|
||||
return $this->deployMultiTarget($repoPath, $srcDir, $liveTargets);
|
||||
}
|
||||
|
||||
$configPath = $this->resolveConfigPath($repoPath);
|
||||
|
||||
$this->log("Repository : {$repoPath}");
|
||||
@@ -130,6 +138,103 @@ class DeploySftp extends CliFramework
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
// ─── Multi-target deploy ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Deploy to multiple live targets from LIVE_TARGETS JSON.
|
||||
*
|
||||
* LIVE_TARGETS format (JSON array of objects):
|
||||
* [
|
||||
* {"host": "web1.example.com", "user": "deploy", "remote_path": "/var/www/module/", "ssh_key_file": "~/.ssh/id_rsa"},
|
||||
* {"host": "web2.example.com", "user": "deploy", "remote_path": "/var/www/module/", "ssh_key_file": "~/.ssh/id_rsa"}
|
||||
* ]
|
||||
*
|
||||
* @return int POSIX exit code (0 if all targets succeed)
|
||||
*/
|
||||
private function deployMultiTarget(string $repoPath, string $srcDir, string $liveTargetsJson): int
|
||||
{
|
||||
$targets = json_decode($liveTargetsJson, true);
|
||||
if (!is_array($targets) || empty($targets)) {
|
||||
$this->log('ERROR', 'LIVE_TARGETS is not a valid JSON array');
|
||||
return self::EXIT_USAGE;
|
||||
}
|
||||
|
||||
$this->section("Multi-target live deploy ({$this->count($targets)} targets)");
|
||||
|
||||
$succeeded = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($targets as $i => $target) {
|
||||
$host = $target['host'] ?? 'unknown';
|
||||
$this->section("Target " . ($i + 1) . ": {$host}");
|
||||
|
||||
// Merge target config into $this->config for this iteration
|
||||
$this->config = $target;
|
||||
|
||||
if (!$this->validateConfig()) {
|
||||
$this->log('ERROR', "Skipping target {$host} — invalid config");
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$remotePath = rtrim((string) $this->config['remote_path'], '/');
|
||||
$ignores = array_merge(
|
||||
$this->buildIgnorePatterns(),
|
||||
$this->loadFtpIgnorePatterns($srcDir),
|
||||
$this->loadFtpIgnorePatterns($repoPath)
|
||||
);
|
||||
|
||||
$user = (string) $this->config['user'];
|
||||
$port = (int) ($this->config['port'] ?? 22);
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->log("[DRY RUN] Would deploy to {$user}@{$host}:{$port} → {$remotePath}");
|
||||
$succeeded++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$sftp = $this->connect($host, $port, $user, $repoPath);
|
||||
if ($sftp === null) {
|
||||
$this->log('ERROR', "Failed to connect to {$host}");
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset counters per target
|
||||
$this->uploaded = 0;
|
||||
$this->skipped = 0;
|
||||
$this->unchanged = 0;
|
||||
$this->deleted = 0;
|
||||
|
||||
$dirCheck = @$sftp->nlist(dirname($remotePath));
|
||||
$baseName = basename($remotePath);
|
||||
$dirExists = is_array($dirCheck) && in_array($baseName, $dirCheck, true);
|
||||
if (!$dirExists) {
|
||||
$sftp->mkdir($remotePath, -1, true);
|
||||
}
|
||||
|
||||
$exitCode = $this->uploadDirectory($sftp, $srcDir, $remotePath, $srcDir, $ignores);
|
||||
|
||||
$this->log(" {$host}: Uploaded={$this->uploaded} Unchanged={$this->unchanged} Deleted={$this->deleted} Skipped={$this->skipped}");
|
||||
|
||||
if ($exitCode === 0) {
|
||||
$succeeded++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->section('Multi-target summary');
|
||||
$this->log("Succeeded: {$succeeded}, Failed: {$failed}");
|
||||
|
||||
return $failed > 0 ? self::EXIT_FAILURE : self::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function count(array $arr): int
|
||||
{
|
||||
return \count($arr);
|
||||
}
|
||||
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -171,8 +276,10 @@ class DeploySftp extends CliFramework
|
||||
|
||||
/** Map of --env values to their sftp-config filename. */
|
||||
private const ENV_CONFIG_MAP = [
|
||||
'dev' => 'sftp-config.dev.json',
|
||||
'rs' => 'sftp-config.rs.json',
|
||||
'dev' => 'sftp-config.dev.json',
|
||||
'rs' => 'sftp-config.rs.json',
|
||||
'demo' => 'sftp-config.demo.json',
|
||||
'live' => 'sftp-config.live.json',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user