feat: deploy-sftp.php supports --env demo and --env live with multi-instance (#184)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s

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