Update UpdateSoftwareCommand.php

This commit is contained in:
M011N1N6 2025-03-06 03:02:07 +03:30 committed by GitHub
parent 177873d5e4
commit ad803b0961
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,7 +14,7 @@ use Symfony\Component\Lock\LockFactory;
#[AsCommand( #[AsCommand(
name: 'hesabix:update', name: 'hesabix:update',
description: 'Updates the Hesabix Core by pulling from GitHub, clearing cache, and updating the database.' description: 'Updates the software by pulling from GitHub, clearing cache, and updating the database.'
)] )]
class UpdateSoftwareCommand extends Command class UpdateSoftwareCommand extends Command
{ {
@ -25,6 +25,7 @@ class UpdateSoftwareCommand extends Command
private string $archiveDir; private string $archiveDir;
private string $backupDir; private string $backupDir;
private string $stateFile; private string $stateFile;
private string $env;
public function __construct(LoggerInterface $logger, LockFactory $lockFactory) public function __construct(LoggerInterface $logger, LockFactory $lockFactory)
{ {
@ -35,6 +36,7 @@ class UpdateSoftwareCommand extends Command
$this->archiveDir = $this->rootDir . '/hesabixArchive'; $this->archiveDir = $this->rootDir . '/hesabixArchive';
$this->backupDir = $this->rootDir . '/../backup'; $this->backupDir = $this->rootDir . '/../backup';
$this->stateFile = $this->backupDir . '/update_state.json'; $this->stateFile = $this->backupDir . '/update_state.json';
$this->env = getenv('APP_ENV') ?: 'prod'; // گرفتن محیط فعلی
parent::__construct(); parent::__construct();
} }
@ -48,7 +50,7 @@ class UpdateSoftwareCommand extends Command
} }
$uuid = Uuid::uuid4()->toString(); $uuid = Uuid::uuid4()->toString();
$this->logger->info("Starting software update with UUID: $uuid"); $this->logger->info("Starting software update with UUID: $uuid in {$this->env} mode");
$this->writeOutput($output, "Starting software update (UUID: $uuid)..."); $this->writeOutput($output, "Starting software update (UUID: $uuid)...");
if ($this->isUpToDate()) { if ($this->isUpToDate()) {
@ -75,7 +77,7 @@ class UpdateSoftwareCommand extends Command
if (!in_array('pre_checks', $state['completedSteps'])) { if (!in_array('pre_checks', $state['completedSteps'])) {
$this->preUpdateChecks($output); $this->preUpdateChecks($output);
$state['completedSteps'][] = 'pre_checks'; $state['completedSteps'][] = 'pre_checks';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Pre-update checks completed');
} }
if (!in_array('archive_backup', $state['completedSteps'])) { if (!in_array('archive_backup', $state['completedSteps'])) {
@ -85,7 +87,7 @@ class UpdateSoftwareCommand extends Command
$archiveHashBefore = $this->getDirectoryHash($this->archiveDir); $archiveHashBefore = $this->getDirectoryHash($this->archiveDir);
$state['archiveHashBefore'] = $archiveHashBefore; $state['archiveHashBefore'] = $archiveHashBefore;
$state['completedSteps'][] = 'archive_backup'; $state['completedSteps'][] = 'archive_backup';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'hesabixArchive backed up');
} else { } else {
$archiveBackup = $state['archiveBackup']; $archiveBackup = $state['archiveBackup'];
$archiveHashBefore = $state['archiveHashBefore']; $archiveHashBefore = $state['archiveHashBefore'];
@ -97,16 +99,20 @@ class UpdateSoftwareCommand extends Command
$this->runProcess(['git', 'pull'], $this->rootDir, $output, 3); $this->runProcess(['git', 'pull'], $this->rootDir, $output, 3);
$state['gitHeadBefore'] = $gitHeadBefore; $state['gitHeadBefore'] = $gitHeadBefore;
$state['completedSteps'][] = 'git_pull'; $state['completedSteps'][] = 'git_pull';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Git pull completed');
} else { } else {
$gitHeadBefore = $state['gitHeadBefore']; $gitHeadBefore = $state['gitHeadBefore'];
} }
if (!in_array('composer_install', $state['completedSteps'])) { if (!in_array('composer_install', $state['completedSteps'])) {
$this->writeOutput($output, 'Installing dependencies...'); $this->writeOutput($output, 'Installing dependencies...');
$this->runProcess(['composer', 'install', '--no-dev', '--optimize-autoloader'], $this->appDir, $output, 3); $composerCommand = ['composer', 'install', '--optimize-autoloader'];
if ($this->env !== 'dev') {
$composerCommand[] = '--no-dev';
}
$this->runProcess($composerCommand, $this->appDir, $output, 3);
$state['completedSteps'][] = 'composer_install'; $state['completedSteps'][] = 'composer_install';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Dependencies installed');
} }
if (!in_array('cache_clear', $state['completedSteps'])) { if (!in_array('cache_clear', $state['completedSteps'])) {
@ -114,9 +120,9 @@ class UpdateSoftwareCommand extends Command
$cacheDir = $this->appDir . '/var/cache'; $cacheDir = $this->appDir . '/var/cache';
$cacheBackup = $this->backupCache($cacheDir); $cacheBackup = $this->backupCache($cacheDir);
$state['cacheBackup'] = $cacheBackup; $state['cacheBackup'] = $cacheBackup;
$this->runProcess(['php', 'bin/console', 'cache:clear', '--env=prod'], $this->appDir, $output, 3); $this->runProcess(['php', 'bin/console', 'cache:clear', "--env={$this->env}"], $this->appDir, $output, 3);
$state['completedSteps'][] = 'cache_clear'; $state['completedSteps'][] = 'cache_clear';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Cache cleared');
} else { } else {
$cacheBackup = $state['cacheBackup']; $cacheBackup = $state['cacheBackup'];
} }
@ -127,7 +133,7 @@ class UpdateSoftwareCommand extends Command
$state['dbBackup'] = $dbBackup; $state['dbBackup'] = $dbBackup;
$this->runProcess(['php', 'bin/console', 'doctrine:schema:update', '--force', '--no-interaction'], $this->appDir, $output, 3); $this->runProcess(['php', 'bin/console', 'doctrine:schema:update', '--force', '--no-interaction'], $this->appDir, $output, 3);
$state['completedSteps'][] = 'db_update'; $state['completedSteps'][] = 'db_update';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Database schema updated');
} else { } else {
$dbBackup = $state['dbBackup']; $dbBackup = $state['dbBackup'];
} }
@ -142,13 +148,13 @@ class UpdateSoftwareCommand extends Command
$this->writeOutput($output, 'hesabixArchive unchanged, no restore needed.'); $this->writeOutput($output, 'hesabixArchive unchanged, no restore needed.');
} }
$state['completedSteps'][] = 'archive_check'; $state['completedSteps'][] = 'archive_check';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Archive check completed');
} }
if (!in_array('post_update_test', $state['completedSteps'])) { if (!in_array('post_update_test', $state['completedSteps'])) {
$this->postUpdateChecks($output); $this->postUpdateChecks($output);
$state['completedSteps'][] = 'post_update_test'; $state['completedSteps'][] = 'post_update_test';
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Post-update tests completed');
} }
$version = $this->getPackageVersion(); $version = $this->getPackageVersion();
@ -157,7 +163,7 @@ class UpdateSoftwareCommand extends Command
$this->logger->info('Software update completed successfully!'); $this->logger->info('Software update completed successfully!');
$this->writeOutput($output, '<info>Software update completed successfully!</info>'); $this->writeOutput($output, '<info>Software update completed successfully!</info>');
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Update completed successfully');
return Command::SUCCESS; return Command::SUCCESS;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Update failed: ' . $e->getMessage()); $this->logger->error('Update failed: ' . $e->getMessage());
@ -165,7 +171,7 @@ class UpdateSoftwareCommand extends Command
$this->rollback($gitHeadBefore, $cacheBackup, $dbBackup, $archiveBackup, $output); $this->rollback($gitHeadBefore, $cacheBackup, $dbBackup, $archiveBackup, $output);
$this->writeOutput($output, '<comment>Update process aborted and rolled back.</comment>'); $this->writeOutput($output, '<comment>Update process aborted and rolled back.</comment>');
$state['error'] = $e->getMessage(); $state['error'] = $e->getMessage();
$this->saveState($uuid, $state); $this->saveState($uuid, $state, $output, 'Update failed and rolled back');
return Command::FAILURE; return Command::FAILURE;
} finally { } finally {
$this->cleanupBackups($cacheBackup, $dbBackup, $archiveBackup); $this->cleanupBackups($cacheBackup, $dbBackup, $archiveBackup);
@ -179,7 +185,6 @@ class UpdateSoftwareCommand extends Command
private function writeOutput(OutputInterface $output, string $message): void private function writeOutput(OutputInterface $output, string $message): void
{ {
$output->writeln($message); $output->writeln($message);
// فقط اگه بافرینگ فعال باشه، flush کن
if (ob_get_level() > 0) { if (ob_get_level() > 0) {
ob_flush(); ob_flush();
flush(); flush();
@ -204,6 +209,11 @@ class UpdateSoftwareCommand extends Command
$this->logger->warning("Attempt $attempt failed for " . implode(' ', $command) . ": $errorMessage"); $this->logger->warning("Attempt $attempt failed for " . implode(' ', $command) . ": $errorMessage");
$this->writeOutput($output, "<comment>Attempt $attempt failed: $errorMessage</comment>"); $this->writeOutput($output, "<comment>Attempt $attempt failed: $errorMessage</comment>");
if ($attempt === $retries) { if ($attempt === $retries) {
if (str_contains($errorMessage, 'symfony-cmd: not found')) {
$this->writeOutput($output, '<comment>Symfony command not found, skipping post-install scripts.</comment>');
$this->logger->warning('Skipping Composer post-install scripts due to missing symfony-cmd.');
return;
}
throw new \RuntimeException('Command "' . implode(' ', $command) . '" failed after ' . $retries . ' attempts: ' . $errorMessage); throw new \RuntimeException('Command "' . implode(' ', $command) . '" failed after ' . $retries . ' attempts: ' . $errorMessage);
} }
sleep(5); sleep(5);
@ -213,22 +223,29 @@ class UpdateSoftwareCommand extends Command
private function isUpToDate(): bool private function isUpToDate(): bool
{ {
try {
$localHeadProcess = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir); $localHeadProcess = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$localHeadProcess->run(); $localHeadProcess->run();
if (!$localHeadProcess->isSuccessful()) { if (!$localHeadProcess->isSuccessful()) {
throw new \RuntimeException('Failed to get local Git HEAD: ' . $localHeadProcess->getErrorOutput()); $this->logger->warning('Failed to get local Git HEAD: ' . $localHeadProcess->getErrorOutput());
return false;
} }
$localHead = trim($localHeadProcess->getOutput()); $localHead = trim($localHeadProcess->getOutput());
$remoteHeadProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $this->rootDir); $remoteHeadProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $this->rootDir);
$remoteHeadProcess->run(); $remoteHeadProcess->run();
if (!$remoteHeadProcess->isSuccessful()) { if (!$remoteHeadProcess->isSuccessful()) {
throw new \RuntimeException('Failed to get remote Git HEAD: ' . $remoteHeadProcess->getErrorOutput()); $this->logger->warning('Failed to get remote Git HEAD: ' . $remoteHeadProcess->getErrorOutput());
return false;
} }
$remoteOutput = explode("\t", trim($remoteHeadProcess->getOutput())); $remoteOutput = explode("\t", trim($remoteHeadProcess->getOutput()));
$remoteHead = $remoteOutput[0] ?? ''; $remoteHead = $remoteOutput[0] ?? '';
return $localHead === $remoteHead; return $localHead === $remoteHead;
} catch (\Exception $e) {
$this->logger->warning('Error checking Git status: ' . $e->getMessage());
return false;
}
} }
private function preUpdateChecks(OutputInterface $output): void private function preUpdateChecks(OutputInterface $output): void
@ -243,7 +260,7 @@ class UpdateSoftwareCommand extends Command
private function postUpdateChecks(OutputInterface $output): void private function postUpdateChecks(OutputInterface $output): void
{ {
$this->writeOutput($output, 'Running post-update tests...'); $this->writeOutput($output, 'Running post-update tests...');
$this->runProcess(['php', 'bin/console', 'cache:warmup', '--env=prod'], $this->appDir, $output); $this->runProcess(['php', 'bin/console', 'cache:warmup', "--env={$this->env}"], $this->appDir, $output);
$this->writeOutput($output, 'Application tested and warmed up successfully.'); $this->writeOutput($output, 'Application tested and warmed up successfully.');
} }
@ -381,10 +398,10 @@ class UpdateSoftwareCommand extends Command
return ['uuid' => $uuid, 'log' => '', 'completedSteps' => []]; return ['uuid' => $uuid, 'log' => '', 'completedSteps' => []];
} }
private function saveState(string $uuid, array $state): void private function saveState(string $uuid, array $state, OutputInterface $output, string $message): void
{ {
$state['uuid'] = $uuid; $state['uuid'] = $uuid;
$state['log'] .= $this->getOutput()->getOutput() . "\n"; $state['log'] .= $output->getVerbosity() >= OutputInterface::VERBOSITY_NORMAL ? $message . "\n" : '';
file_put_contents($this->stateFile, json_encode($state)); file_put_contents($this->stateFile, json_encode($state));
} }
} }