Deployment-Services implementiert: Basisklasse, Git-spezifische Implementierung und Resolver hinzugefügt
This commit is contained in:
26
src/Service/Deployment/DeploymentInterface.php
Normal file
26
src/Service/Deployment/DeploymentInterface.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* DeploymentInterface.php 2026-03-20 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
72
src/Service/Deployment/DeploymentService.php
Normal file
72
src/Service/Deployment/DeploymentService.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* DeploymentService.php 2026-03-23 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Service/Deployment/DeploymentServiceResolver.php
Normal file
40
src/Service/Deployment/DeploymentServiceResolver.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* DeploymentServiceResolver.php 2026-03-15 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/Service/Git/GitDeployment.php
Normal file
110
src/Service/Git/GitDeployment.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* GitDeployment.php 2026-03-25 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/Service/Git/GitService.php
Normal file
138
src/Service/Git/GitService.php
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* GitService.php 2026-03-25 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user