diff --git a/deploy/deploy-sftp.php b/deploy/deploy-sftp.php index 48bb5ce..9f0f996 100644 --- a/deploy/deploy-sftp.php +++ b/deploy/deploy-sftp.php @@ -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', ]; /** diff --git a/templates/scripts/deploy/sftp-config.demo.json.example b/templates/scripts/deploy/sftp-config.demo.json.example new file mode 100644 index 0000000..36c57be --- /dev/null +++ b/templates/scripts/deploy/sftp-config.demo.json.example @@ -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 +} diff --git a/templates/scripts/deploy/sftp-config.live.json.example b/templates/scripts/deploy/sftp-config.live.json.example new file mode 100644 index 0000000..90914e4 --- /dev/null +++ b/templates/scripts/deploy/sftp-config.live.json.example @@ -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 +}