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
This commit is contained in:
DevSoftTech Server 2026-08-04 03:17:46 +05:30
parent cb4636e599
commit f3b8e28312
4 changed files with 170 additions and 3 deletions

View File

@ -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');

View File

@ -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;

View File

@ -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('<?= base_url('setting/admin/system-ops/update') ?>', {}, 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('<?= base_url('setting/admin/system-ops/update') ?>', {}, 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('<?= base_url('setting/admin/system-ops/updateDirect') ?>', {}, this); })
.on('click', '#btnRestartWeb', function(){ if (!confirm('Restart the web server now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'restart_web'}, this); })
.on('click', '#btnRestartPhp', function(){ if (!confirm('Restart PHP-FPM now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'restart_php'}, this); })
.on('click', '#btnReboot', function(){ if (!confirm('Reboot the server now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'reboot'}, this); })

View File

@ -44,7 +44,11 @@
<h5 class="mb-0">Maintenance Actions</h5>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary" id="btnUpdateSystem">Update HMS</button>
<div class="d-grid gap-2">
<button class="btn btn-primary" id="btnUpdateSystem" title="Update via git (requires git configuration)">Update HMS (Git)</button>
<button class="btn btn-success" id="btnUpdateDirectGithub" title="Download and deploy latest from GitHub (more reliable)">Deploy from GitHub</button>
</div>
<hr class="my-2">
<button class="btn btn-outline-warning" id="btnRestartWeb">Restart Web Server</button>
<button class="btn btn-outline-info" id="btnRestartPhp">Restart PHP-FPM</button>
<button class="btn btn-outline-danger" id="btnShutdown">Shutdown Server</button>