From f3b8e28312b475a6b246a589e50238be5f460176 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Tue, 4 Aug 2026 03:17:46 +0530 Subject: [PATCH] feat: add direct GitHub deployment as alternative to git pull - New 'Deploy from GitHub' button downloads latest code directly - No git configuration required - more reliable for git-unfriendly servers - Automatically syncs changed files (skips .git, vendor, writable) - Both methods available: git pull (if configured) or direct GitHub download - Each has a clear confirmation dialog explaining the method - Handles temporary extraction and cleanup automatically This solves HMS deployment when git is not accessible to PHP process --- app/Controllers/SystemOps.php | 8 + app/Libraries/SystemOperations.php | 154 +++++++++++++++++++ app/Views/Setting/Admin/system_ops.php | 5 +- app/Views/Setting/Admin/system_ops_panel.php | 6 +- 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/app/Controllers/SystemOps.php b/app/Controllers/SystemOps.php index b95039a..b0f6ca5 100644 --- a/app/Controllers/SystemOps.php +++ b/app/Controllers/SystemOps.php @@ -37,6 +37,14 @@ class SystemOps extends BaseController return $this->response->setJSON($result); } + public function updateDirect() + { + $ops = new SystemOperations(); + $result = $ops->runUpdateDirect(); + + return $this->response->setJSON($result); + } + public function action() { $action = (string) $this->request->getPost('action'); diff --git a/app/Libraries/SystemOperations.php b/app/Libraries/SystemOperations.php index 6ffffc7..a7b39de 100644 --- a/app/Libraries/SystemOperations.php +++ b/app/Libraries/SystemOperations.php @@ -41,6 +41,160 @@ class SystemOperations return $status; } + public function runUpdateDirect(): array + { + $repoPath = ROOTPATH; + $logFile = WRITEPATH . 'system_update.log'; + $startedAt = date('Y-m-d H:i:s'); + + $entry = [ + 'timestamp' => $startedAt, + 'type' => 'update', + 'status' => 'running', + 'message' => 'HMS update started (direct deployment from GitHub)', + ]; + $this->appendHistory($entry); + + try { + // GitHub repo details - PUBLIC REPO, no auth needed + $githubOwner = 'infodevsofttech-admin'; + $githubRepo = 'devsofttech_hms_ci4'; + $branch = 'main'; + + // Download latest ZIP from GitHub + $zipUrl = "https://github.com/{$githubOwner}/{$githubRepo}/archive/refs/heads/{$branch}.zip"; + $tempDir = sys_get_temp_dir(); + $zipFile = $tempDir . '/hms_update_' . time() . '.zip'; + + file_put_contents($logFile, "Downloading from: {$zipUrl}\n", FILE_APPEND); + + $zipContent = @file_get_contents($zipUrl); + if ($zipContent === false) { + throw new \Exception('Failed to download from GitHub. Check network connectivity and GitHub availability.'); + } + + if (!file_put_contents($zipFile, $zipContent)) { + throw new \Exception('Failed to write downloaded ZIP to temporary directory.'); + } + + file_put_contents($logFile, "Downloaded ZIP: {$zipFile} (" . filesize($zipFile) . " bytes)\n", FILE_APPEND); + + // Extract ZIP + $zip = new \ZipArchive(); + if ($zip->open($zipFile) !== true) { + throw new \Exception('Failed to open downloaded ZIP file.'); + } + + $extractDir = $tempDir . '/hms_extract_' . time(); + if (!mkdir($extractDir)) { + throw new \Exception('Failed to create extraction directory.'); + } + + $zip->extractTo($extractDir); + $zip->close(); + + // Find the extracted directory (GitHub creates a folder like "repo-main") + $dirs = array_diff(scandir($extractDir), ['.', '..']); + if (empty($dirs)) { + throw new \Exception('Downloaded ZIP appears to be empty.'); + } + + $extractedRepoDir = $extractDir . '/' . reset($dirs); + file_put_contents($logFile, "Extracted to: {$extractedRepoDir}\n", FILE_APPEND); + + // Sync files (skip git, vendor, and certain local files) + $this->syncFiles($extractedRepoDir, $repoPath, $logFile); + + // Cleanup + $this->deleteDirectory($extractDir); + @unlink($zipFile); + + $successMsg = 'HMS updated successfully from GitHub (direct deployment)'; + $this->appendHistory([ + 'timestamp' => date('Y-m-d H:i:s'), + 'type' => 'update', + 'status' => 'success', + 'message' => $successMsg, + 'detail' => 'Files synced from ' . $zipUrl, + ]); + file_put_contents($logFile, "SUCCESS: {$successMsg}\n", FILE_APPEND); + + return ['ok' => true, 'message' => $successMsg]; + } catch (\Throwable $e) { + $errorMsg = 'Update failed: ' . $e->getMessage(); + $this->appendHistory([ + 'timestamp' => date('Y-m-d H:i:s'), + 'type' => 'update', + 'status' => 'failed', + 'message' => 'HMS update failed', + 'detail' => $errorMsg, + ]); + file_put_contents($logFile, "FAILED: {$errorMsg}\n", FILE_APPEND); + return ['ok' => false, 'message' => $errorMsg]; + } + } + + private function syncFiles(string $sourceDir, string $destDir, string $logFile): void + { + $skip = ['.git', '.gitignore', 'vendor', 'node_modules', 'writable', '.env', '.env.example']; + + $this->copyRecursive($sourceDir, $destDir, $skip, $logFile); + file_put_contents($logFile, "File sync completed\n", FILE_APPEND); + } + + private function copyRecursive(string $src, string $dest, array $skip, string $logFile): void + { + $dir = opendir($src); + if (!$dir) { + throw new \Exception("Cannot open directory: {$src}"); + } + + while (false !== ($file = readdir($dir))) { + if ($file === '.' || $file === '..') { + continue; + } + + if (in_array($file, $skip)) { + continue; + } + + $srcPath = $src . '/' . $file; + $destPath = $dest . '/' . $file; + + if (is_dir($srcPath)) { + if (!is_dir($destPath)) { + @mkdir($destPath, 0755, true); + } + $this->copyRecursive($srcPath, $destPath, $skip, $logFile); + } else { + if (@copy($srcPath, $destPath)) { + file_put_contents($logFile, " ✓ {$file}\n", FILE_APPEND); + } else { + file_put_contents($logFile, " ✗ {$file} (copy failed)\n", FILE_APPEND); + } + } + } + closedir($dir); + } + + private function deleteDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->deleteDirectory($path); + } else { + @unlink($path); + } + } + @rmdir($dir); + } + public function runUpdate(): array { $repoPath = ROOTPATH; diff --git a/app/Views/Setting/Admin/system_ops.php b/app/Views/Setting/Admin/system_ops.php index dd81975..5db046c 100644 --- a/app/Views/Setting/Admin/system_ops.php +++ b/app/Views/Setting/Admin/system_ops.php @@ -88,8 +88,9 @@ setHealthState('healthy'); setInterval(function(){ refreshPanel(); }, 30000); - $(document).off('click', '#btnUpdateSystem, #btnRestartWeb, #btnRestartPhp, #btnReboot, #btnShutdown') - .on('click', '#btnUpdateSystem', function(){ if (!confirm('Pull latest HMS code from repository?')) return; postAction('', {}, this); }) + $(document).off('click', '#btnUpdateSystem, #btnUpdateDirectGithub, #btnRestartWeb, #btnRestartPhp, #btnReboot, #btnShutdown') + .on('click', '#btnUpdateSystem', function(){ if (!confirm('Pull latest HMS code from repository?\n\nThis method requires git to be configured on the server.')) return; postAction('', {}, this); }) + .on('click', '#btnUpdateDirectGithub', function(){ if (!confirm('Deploy latest HMS code from GitHub?\n\nThis will download the latest code and update files.\nThis method is more reliable if git is not available.')) return; postAction('', {}, this); }) .on('click', '#btnRestartWeb', function(){ if (!confirm('Restart the web server now?')) return; postAction('', {action: 'restart_web'}, this); }) .on('click', '#btnRestartPhp', function(){ if (!confirm('Restart PHP-FPM now?')) return; postAction('', {action: 'restart_php'}, this); }) .on('click', '#btnReboot', function(){ if (!confirm('Reboot the server now?')) return; postAction('', {action: 'reboot'}, this); }) diff --git a/app/Views/Setting/Admin/system_ops_panel.php b/app/Views/Setting/Admin/system_ops_panel.php index 95bcc5b..5d36e84 100644 --- a/app/Views/Setting/Admin/system_ops_panel.php +++ b/app/Views/Setting/Admin/system_ops_panel.php @@ -44,7 +44,11 @@
Maintenance Actions
- +
+ + +
+