From 90d61cdfb8fbba05dea1509fb2bc4bc6698b6a35 Mon Sep 17 00:00:00 2001 From: Thomas Schneider Date: Fri, 27 Mar 2026 14:25:30 +0100 Subject: [PATCH] =?UTF-8?q?Deployment-Services=20implementiert:=20Basiskla?= =?UTF-8?q?sse,=20Git-spezifische=20Implementierung=20und=20Resolver=20hin?= =?UTF-8?q?zugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Deployment/DeploymentInterface.php | 26 ++++ src/Service/Deployment/DeploymentService.php | 72 +++++++++ .../Deployment/DeploymentServiceResolver.php | 40 +++++ src/Service/Git/GitDeployment.php | 110 ++++++++++++++ src/Service/Git/GitService.php | 138 ++++++++++++++++++ 5 files changed, 386 insertions(+) create mode 100644 src/Service/Deployment/DeploymentInterface.php create mode 100644 src/Service/Deployment/DeploymentService.php create mode 100644 src/Service/Deployment/DeploymentServiceResolver.php create mode 100644 src/Service/Git/GitDeployment.php create mode 100644 src/Service/Git/GitService.php diff --git a/src/Service/Deployment/DeploymentInterface.php b/src/Service/Deployment/DeploymentInterface.php new file mode 100644 index 0000000..7c61421 --- /dev/null +++ b/src/Service/Deployment/DeploymentInterface.php @@ -0,0 +1,26 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Service\Deployment; + +use App\Entity\Project; + +interface DeploymentInterface +{ + public function supports(): string; + + public function init(Project $project): void; + + public function handle(Project $project, string $version = '', string $step = ''): void; + + public function fetch(Project $project): void; + + public function deploy(Project $project, string $version): void; + + public function cleanup(Project $project): void; +} diff --git a/src/Service/Deployment/DeploymentService.php b/src/Service/Deployment/DeploymentService.php new file mode 100644 index 0000000..ab4d35e --- /dev/null +++ b/src/Service/Deployment/DeploymentService.php @@ -0,0 +1,72 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Service\Deployment; + +use App\Entity\Project; +use App\Service\Docker\DockerCompose; +use App\Service\ProjectConfigDirService; +use App\Traits\LoggerTrait; +use Exception; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Contracts\Service\Attribute\Required; + +#[AutoconfigureTag('app.deployment_service')] +abstract class DeploymentService implements DeploymentInterface +{ + use LoggerTrait; + + protected ProjectConfigDirService $configDirService; + + protected string $filesDir = 'files.deploy'; + + #[Required] + public function setDependencies( + ProjectConfigDirService $configDirService + ): void + { + $this->configDirService = $configDirService; + } + + + public function deploy(Project $project, string $version): void + { + new DockerCompose() + ->setName($project->name) + ->setWorkingDir($project->projectDir) + ->setComposeFiles([$this->filesDir . '/compose.yaml', $this->filesDir . '/compose.prod.yaml']) + ->daemonize(true) + ->up(['--build']); + } + + + public function cleanup(Project $project): void + { + $this->clearDeploymentFiles($project); + } + + + protected function clearDeploymentFiles(Project $project): void + { + $filesystem = new Filesystem(); + if($filesystem->exists($project->projectDir . DIRECTORY_SEPARATOR . $this->filesDir)) + { + $this->logger->info('Removing working tree'); + + try + { + $filesystem->remove($project->projectDir . DIRECTORY_SEPARATOR . $this->filesDir); + } + catch(Exception $e) + { + $this->logger->error('Failed to remove deployment files: ' . $e->getMessage()); + } + } + } +} diff --git a/src/Service/Deployment/DeploymentServiceResolver.php b/src/Service/Deployment/DeploymentServiceResolver.php new file mode 100644 index 0000000..ebe1fd9 --- /dev/null +++ b/src/Service/Deployment/DeploymentServiceResolver.php @@ -0,0 +1,40 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Service\Deployment; + +use App\Entity\Project; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + +class DeploymentServiceResolver +{ + public function __construct( + #[AutowireIterator('app.deployment_service')] protected iterable $deploymentServices, + ){} + + + public function init(Project $project): DeploymentInterface + { + if(empty($project->deployment['type'])) + { + throw new \InvalidArgumentException('Deployment type is not set'); + } + + foreach($this->deploymentServices as $deploymentService) + { + if ($deploymentService->supports() === $project->deployment['type']) + { + $deploymentService->init($project); + return $deploymentService; + } + } + + throw new \InvalidArgumentException('Deployment type is not supported'); + } +} diff --git a/src/Service/Git/GitDeployment.php b/src/Service/Git/GitDeployment.php new file mode 100644 index 0000000..16c04d8 --- /dev/null +++ b/src/Service/Git/GitDeployment.php @@ -0,0 +1,110 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Service\Git; + +use App\Entity\Project; +use App\Service\Deployment\DeploymentService; +use App\Service\ProjectConfigDirService; +use App\Traits\LoggerTrait; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +#[AutoconfigureTag('app.deployment_service', ['git'])] +class GitDeployment extends DeploymentService +{ + use LoggerTrait; + + const string GIT_DIR = 'repo.git'; + + + public function __construct( + protected GitService $git, private readonly ProjectConfigDirService $projectConfigDirService, + ){} + + + public function supports(): string + { + return 'git'; + } + + + public function init(Project $project): void + { + $this->git->setWorkingDir($project->projectDir); + } + + + public function handle(Project $project, string $version = '', string $step = ''): void + { + $this->fetch($project); + $this->deploy($project, $version); + } + + + public function fetch(Project $project): void + { + $this->projectConfigDirService->setDeploymentStep($project->projectDir, 'fetch'); + $options = []; + + if(!empty($project->deployment['repository_url']) && str_starts_with($project->deployment['repository_url'], 'http')) + { + $options['auth_basic'] = [ + 'username' => $project->deployment['username'] ?? '', + 'password' => $project->deployment['password'] ?? '', + ]; + } + + if($this->git->isCloned(self::GIT_DIR)) + { + $this->projectConfigDirService->setDeploymentStep($project->projectDir, 'fetch.git.clone'); + + $this->git->fetchRepo( + dir: self::GIT_DIR, + options: $options, + ); + } + else + { + $this->projectConfigDirService->setDeploymentStep($project->projectDir, 'fetch.git.fetch'); + + $this->git->cloneRepo( + repoUrl: $project->deployment['repository_url'], + targetDir: self::GIT_DIR, + options: [...$options, '--mirror'], + ); + } + } + + + public function deploy(Project $project, string $version): void + { + # remove work tree + $this->clearDeploymentFiles($project); + + # create work tree + $this->git->cloneRepo(self::GIT_DIR, $this->filesDir); + + // default translates to current state of default branch + if($version === 'default') + { + $version = $this->git->getRepoDefaultBranch(self::GIT_DIR); + } + + if(!$version || $version === 'latest') + { + # check out latest release + $this->git->checkoutLatestRelease($this->filesDir); + } + else + { + $this->git->checkoutRepo($this->filesDir, $version); + } + + parent::deploy($project, $version); + } +} diff --git a/src/Service/Git/GitService.php b/src/Service/Git/GitService.php new file mode 100644 index 0000000..401895c --- /dev/null +++ b/src/Service/Git/GitService.php @@ -0,0 +1,138 @@ + + * Alle Rechte vorbehalten. + */ + +namespace App\Service\Git; + +use App\Traits\LoggerTrait; +use League\Uri\Uri; +use Symfony\Component\Process\Process; + +class GitService +{ + use LoggerTrait; + + protected string $workingDir; + + public function __construct( + ){ + $this->workingDir = sys_get_temp_dir(); + } + + + public function setWorkingDir(string $dir): self + { + $this->workingDir = $dir; + return $this; + } + + + public function isCloned(string $dir = '.git'): bool + { + return is_dir($this->workingDir . DIRECTORY_SEPARATOR . $dir); + } + + + public function cloneRepo(string $repoUrl, string $targetDir = '', array $options = []): void + { + if(!empty($options['auth_basic'])) + { + $repoUrl = Uri::new($repoUrl) + ->withUsername($options['auth_basic']['username'] ?? '') + ->withPassword($options['auth_basic']['password'] ?? '') + ->toString() + ; + + unset ($options['auth_basic']); + } + + new Process( + command: ['git', 'clone', ...$options, $repoUrl, $targetDir], + cwd: $this->workingDir, + )->mustRun(); + } + + + public function checkoutRepo(string $dir, string $branch = 'master'): void + { + $process = new Process(['git', 'checkout', '-f', $branch]); + $process->setWorkingDirectory($this->workingDir . DIRECTORY_SEPARATOR . $dir); + $process->mustRun(); + } + + + public function fetchRepo(string $dir, array $options = []): void + { + $process = Process::fromShellCommandline('git --all --tags'); + $process->setWorkingDirectory($this->workingDir . DIRECTORY_SEPARATOR . $dir); + $process->mustRun(); + } + + + public function updateRef(string $dir, array $options = []): void + { + Process::fromShellCommandline( + command: 'git update-ref', + cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir + )->mustRun(); + } + + + public function pullRepo(string $targetDir, array $options = []): void + { + Process::fromShellCommandline( + command: 'git pull --all --tags --force', + cwd: $this->workingDir . DIRECTORY_SEPARATOR . $targetDir + )->mustRun(); + } + + + public function getRepoDefaultBranch(string $dir): string + { + $process = Process::fromShellCommandline('git symbolic-ref --short HEAD'); + $process->setWorkingDirectory($this->workingDir . DIRECTORY_SEPARATOR . $dir); + $process->mustRun(); + + return trim($process->getOutput()) ?: 'master'; + } + + + public function getCurrentRelease(string $dir): string + { + $process = Process::fromShellCommandline('git describe --tags'); + $process->setWorkingDirectory($this->workingDir . DIRECTORY_SEPARATOR . $dir); + $process->mustRun(); + + return trim($process->getOutput()); + } + + + public function getLatestRelease(string $dir): string + { + $process = Process::fromShellCommandline('git tag --sort=committerdate --list "v[0-9]*" "[0-9]*.[0-9]*.[0-9]*" | tail -1'); + $process->setWorkingDirectory($this->workingDir . DIRECTORY_SEPARATOR . $dir); + $process->mustRun(); + + return trim($process->getOutput()); + } + + + public function checkoutLatestRelease(string $dir): void + { + $currentRelease = $this->getCurrentRelease($dir); + $latestRelease = $this->getLatestRelease($dir); + + // Update abbrechen, wenn Version bereits aktuell + if($currentRelease === $latestRelease) + { + return; + } + + // neueste Version herunterladen und auschecken + $this->checkoutRepo($dir, $latestRelease); + } +}