Compare commits
10 Commits
61ac3b6527
...
ee1b9bafc7
| Author | SHA1 | Date | |
|---|---|---|---|
| ee1b9bafc7 | |||
| 39042b0236 | |||
| 63189fda5c | |||
| 6320fcc8c0 | |||
| b14e50af1a | |||
| 7ec38a6e98 | |||
| 90d61cdfb8 | |||
| 804bcf9e7a | |||
| 790963976a | |||
| 6e40a0e85e |
@@ -14,6 +14,9 @@ composer_version: "2"
|
|||||||
web_environment: []
|
web_environment: []
|
||||||
corepack_enable: false
|
corepack_enable: false
|
||||||
omit_containers: [db]
|
omit_containers: [db]
|
||||||
|
hooks:
|
||||||
|
pre-start:
|
||||||
|
- exec-host: ddev dotenv set .ddev/.env --docker-dotenv-vars="$(docker compose -f .ddev/.ddev-docker-compose-full.yaml config | awk '/^[[:space:]]{2}web:$/ {inweb=1; next} inweb && /^[[:space:]]{4}environment:$/ {inenv=1; next} inenv && /^[[:space:]]{6}[A-Za-z0-9_]+:/ {gsub(/^[[:space:]]+|:$/, "", $1); print $1; next} inenv && /^[[:space:]]{4}[^[:space:]]/ {inenv=0} inweb && /^[[:space:]]{2}[^[:space:]]/ && !/^[[:space:]]{4}/ {inweb=0}' | sort -u | paste -sd, -)"
|
||||||
|
|
||||||
# Key features of DDEV's config.yaml:
|
# Key features of DDEV's config.yaml:
|
||||||
|
|
||||||
|
|||||||
4
.ddev/docker-compose.debug.yaml
Normal file
4
.ddev/docker-compose.debug.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
environment:
|
||||||
|
- PHP_IDE_CONFIG=serverName=orchestar.ddev.site
|
||||||
6
.idea/copyright/Copyright.xml
generated
Normal file
6
.idea/copyright/Copyright.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="CopyrightManager">
|
||||||
|
<copyright>
|
||||||
|
<option name="notice" value="&#36;file.fileName &#36;file.lastModified.format("Y-MM-d") &#36;username Copyright (c) &#36;originalComment.match("Copyright \(c\) (\d+)", 1, "-", "&#36;today.year")&#36;today.year Thomas Schneider <thomas@inter-mundos.de> Alle Rechte vorbehalten." />
|
||||||
|
<option name="myName" value="Copyright" />
|
||||||
|
</copyright>
|
||||||
|
</component>
|
||||||
7
.idea/copyright/profiles_settings.xml
generated
Normal file
7
.idea/copyright/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="CopyrightManager">
|
||||||
|
<settings default="Copyright">
|
||||||
|
<module2copyright>
|
||||||
|
<element module="Project Files" copyright="Copyright" />
|
||||||
|
</module2copyright>
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"doctrine/doctrine-bundle": "^3.2",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
|
"league/uri": "^7.0",
|
||||||
"phpdocumentor/reflection-docblock": "^6.0",
|
"phpdocumentor/reflection-docblock": "^6.0",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
"sentry/sentry-symfony": "^5.9",
|
"sentry/sentry-symfony": "^5.9",
|
||||||
@@ -20,6 +21,8 @@
|
|||||||
"symfony/doctrine-messenger": "8.0.*",
|
"symfony/doctrine-messenger": "8.0.*",
|
||||||
"symfony/dotenv": "8.0.*",
|
"symfony/dotenv": "8.0.*",
|
||||||
"symfony/expression-language": "8.0.*",
|
"symfony/expression-language": "8.0.*",
|
||||||
|
"symfony/filesystem": "8.0.*",
|
||||||
|
"symfony/finder": "8.0.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/form": "8.0.*",
|
"symfony/form": "8.0.*",
|
||||||
"symfony/framework-bundle": "8.0.*",
|
"symfony/framework-bundle": "8.0.*",
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
"symfony/intl": "8.0.*",
|
"symfony/intl": "8.0.*",
|
||||||
"symfony/mailer": "8.0.*",
|
"symfony/mailer": "8.0.*",
|
||||||
"symfony/mime": "8.0.*",
|
"symfony/mime": "8.0.*",
|
||||||
"symfony/monolog-bundle": "^3.0|^4.0",
|
"symfony/monolog-bundle": "^4.0",
|
||||||
"symfony/notifier": "8.0.*",
|
"symfony/notifier": "8.0.*",
|
||||||
"symfony/process": "8.0.*",
|
"symfony/process": "8.0.*",
|
||||||
"symfony/property-access": "8.0.*",
|
"symfony/property-access": "8.0.*",
|
||||||
@@ -44,7 +47,8 @@
|
|||||||
"symfony/web-link": "8.0.*",
|
"symfony/web-link": "8.0.*",
|
||||||
"symfony/yaml": "8.0.*",
|
"symfony/yaml": "8.0.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0",
|
||||||
|
"vlucas/phpdotenv": "^5.6"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
|||||||
1018
composer.lock
generated
1018
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,15 @@
|
|||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
|
app.data_dir: '%kernel.project_dir%/var/data'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
autowire: true # Automatically injects dependencies in your services.
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
bind:
|
||||||
|
string $dataDir: '%app.data_dir%'
|
||||||
|
|
||||||
# makes classes in src/ available to be used as services
|
# makes classes in src/ available to be used as services
|
||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
|||||||
21
src/Attributes/Mapping/DataFromArray.php
Normal file
21
src/Attributes/Mapping/DataFromArray.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* DataFromArray.php 2026-03-16 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Attributes\Mapping;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||||
|
final readonly class DataFromArray
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string|null $key,
|
||||||
|
){}
|
||||||
|
}
|
||||||
0
src/Entity/.gitignore
vendored
0
src/Entity/.gitignore
vendored
74
src/Entity/Project.php
Normal file
74
src/Entity/Project.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Project.php 2026-03-20 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Attributes\Mapping\DataFromArray;
|
||||||
|
use App\Service\Deployment\DeploymentInterface;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
class Project
|
||||||
|
{
|
||||||
|
#[DataFromArray(key: 'name')]
|
||||||
|
public readonly string $name;
|
||||||
|
|
||||||
|
#[DataFromArray(key: 'deployment')]
|
||||||
|
public readonly array $deployment;
|
||||||
|
|
||||||
|
protected DeploymentInterface $deploymentService;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
array $data,
|
||||||
|
public string $projectDir,
|
||||||
|
public string $deployedVersion,
|
||||||
|
\Closure $deploymentServiceResolver,
|
||||||
|
){
|
||||||
|
$this->autoMapData($data);
|
||||||
|
|
||||||
|
$this->deploymentService = ($deploymentServiceResolver)()->init($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function deploy(string $version = '', string $step = ''): void
|
||||||
|
{
|
||||||
|
$this->deploymentService->handle($this, $version, $step);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically maps data from the provided array to object properties
|
||||||
|
* based on attributes defined in the class.
|
||||||
|
*
|
||||||
|
* @param array $data The input data array containing key-value pairs to map.
|
||||||
|
*/
|
||||||
|
protected function autoMapData(array $data): void
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$class = new ReflectionClass($this);
|
||||||
|
|
||||||
|
foreach($class->getProperties() as $property)
|
||||||
|
{
|
||||||
|
foreach($property->getAttributes() as $attribute)
|
||||||
|
{
|
||||||
|
if($attribute->getName() === DataFromArray::class)
|
||||||
|
{
|
||||||
|
$attributeArguments = $attribute->getArguments();
|
||||||
|
$this->{$property->getName()} = $data[$attributeArguments['key']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ReflectionException $e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/Repository/.gitignore
vendored
0
src/Repository/.gitignore
vendored
57
src/Repository/ProjectRepository.php
Normal file
57
src/Repository/ProjectRepository.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ProjectRepository.php 2026-03-15 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Service\Deployment\DeploymentServiceResolver;
|
||||||
|
use App\Service\ProjectConfigDirService;
|
||||||
|
use Generator;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
class ProjectRepository
|
||||||
|
{
|
||||||
|
/** @var Project[] */
|
||||||
|
private array $projects;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected ProjectConfigDirService $configDirService,
|
||||||
|
#[AutowireServiceClosure(DeploymentServiceResolver::class)] \Closure $deploymentServiceResolver,
|
||||||
|
){
|
||||||
|
foreach($this->configDirService->getProjectDirContents() as $projectFile)
|
||||||
|
{
|
||||||
|
$deployedVersion = $this->configDirService->getDeployedVersion($projectFile->getPath());
|
||||||
|
|
||||||
|
$this->projects[$projectFile->getRelativePath()] = new Project(
|
||||||
|
data: Yaml::parseFile($projectFile->getRealPath()),
|
||||||
|
projectDir: $projectFile->getPath(),
|
||||||
|
deployedVersion: $deployedVersion,
|
||||||
|
deploymentServiceResolver: $deploymentServiceResolver,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getAll(): Generator
|
||||||
|
{
|
||||||
|
yield from $this->projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function findById($id): Project|null
|
||||||
|
{
|
||||||
|
return $this->projects[$id] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function exists(string $id): bool
|
||||||
|
{
|
||||||
|
return array_any($this->projects, fn($project) => $project['id'] === $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
371
src/Service/Docker/DockerCompose.php
Normal file
371
src/Service/Docker/DockerCompose.php
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* DockerCompose.php 2026-03-27 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Service\Docker;
|
||||||
|
|
||||||
|
use App\Service\Process\CleanProcess;
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
use Exception;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
class DockerCompose
|
||||||
|
{
|
||||||
|
protected string $composeCmd = 'compose';
|
||||||
|
|
||||||
|
protected float $startCommandTimeout = 600;
|
||||||
|
|
||||||
|
protected string $name = '';
|
||||||
|
|
||||||
|
protected bool $daemonize = false;
|
||||||
|
|
||||||
|
protected string $dockerHost = '';
|
||||||
|
|
||||||
|
protected string $workingDir = '';
|
||||||
|
|
||||||
|
|
||||||
|
protected array $composeFiles = [];
|
||||||
|
|
||||||
|
protected array $envFiles = [];
|
||||||
|
|
||||||
|
protected array $inlineEnvFiles = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Fehlerüberprüfung /-ausgabe optimieren
|
||||||
|
*/
|
||||||
|
public function up(array $optionalArgs = []): void
|
||||||
|
{
|
||||||
|
$process = $this->process('up', $optionalArgs);
|
||||||
|
$process->run(function ($type, $buffer): void
|
||||||
|
{
|
||||||
|
echo $buffer;
|
||||||
|
});
|
||||||
|
|
||||||
|
// if(!$process->isSuccessful())
|
||||||
|
// {
|
||||||
|
// throw CouldNotStartDockerContainer::processFailed($this, $process);
|
||||||
|
// }
|
||||||
|
|
||||||
|
$error = $process->getErrorOutput();
|
||||||
|
|
||||||
|
$dockerIdentifier = trim($process->getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a process instance for the given command.
|
||||||
|
*
|
||||||
|
* @param string $cmd The command to execute.
|
||||||
|
* @param array $optionalArgs Optional arguments to be appended to the command.
|
||||||
|
*
|
||||||
|
* @return Process The configured process instance.
|
||||||
|
*
|
||||||
|
* @todo Improve error handling and validation of environment variables.
|
||||||
|
* @todo Evaluate potential performance issues with environment loading and inline parsing.
|
||||||
|
*/
|
||||||
|
protected function process(string $cmd, array $optionalArgs = []): Process
|
||||||
|
{
|
||||||
|
$env = [...$this->loadDotEnv(), ...$this->parseInlineEnvFiles()];
|
||||||
|
|
||||||
|
if(!empty($env['DOCKER_HOST']))
|
||||||
|
{
|
||||||
|
$this->dockerHost = $env['DOCKER_HOST'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return CleanProcess::fromShellCommandline(
|
||||||
|
command: implode(' ', [...$this->getBaseCommand(), $cmd, ...$this->getExtraOptions($optionalArgs)]),
|
||||||
|
cwd: $this->workingDir,
|
||||||
|
env: $env,
|
||||||
|
timeout: $this->startCommandTimeout,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs and returns the base command array for the Docker Compose operations.
|
||||||
|
*
|
||||||
|
* @return array The assembled base command array, including the base compose command
|
||||||
|
* and additional Docker options.
|
||||||
|
*
|
||||||
|
* @todo Evaluate potential optimizations in command composition and handling.
|
||||||
|
*/
|
||||||
|
protected function getBaseCommand(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...explode(' ', $this->composeCmd),
|
||||||
|
...$this->getExtraDockerOptions(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Review the logic for constructing extra Docker options, ensuring all possible options are correctly appended.
|
||||||
|
* @todo Validate and sanitize the inputs for docker host, name, compose files, and environment files if necessary.
|
||||||
|
* @todo Consider handling potential edge cases where input values might contain unexpected characters or formats.
|
||||||
|
*/
|
||||||
|
protected function getExtraDockerOptions(): array
|
||||||
|
{
|
||||||
|
$extraDockerOptions = [];
|
||||||
|
|
||||||
|
if($this->dockerHost !== '')
|
||||||
|
{
|
||||||
|
$extraDockerOptions[] = "-H {$this->dockerHost}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->name !== '')
|
||||||
|
{
|
||||||
|
$extraDockerOptions[] = "-p {$this->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->composeFiles)
|
||||||
|
{
|
||||||
|
foreach($this->composeFiles as $file)
|
||||||
|
{
|
||||||
|
$extraDockerOptions[] = "--file {$file}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->envFiles)
|
||||||
|
{
|
||||||
|
foreach($this->envFiles as $file)
|
||||||
|
{
|
||||||
|
$extraDockerOptions[] = "--env-file {$file}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $extraDockerOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves additional options for the current process.
|
||||||
|
*
|
||||||
|
* If the application is running in daemon mode and the '-d' option
|
||||||
|
* is not already set, it will be appended to the resulting options array.
|
||||||
|
*
|
||||||
|
* @param array $optionalArgs Optional arguments to customize the options.
|
||||||
|
* @return array Merged array of optional arguments and inferred additional options.
|
||||||
|
*/
|
||||||
|
protected function getExtraOptions(array $optionalArgs = []): array
|
||||||
|
{
|
||||||
|
$extraOptions = [...$optionalArgs];
|
||||||
|
|
||||||
|
if($this->daemonize && !in_array('-d', $extraOptions, true))
|
||||||
|
{
|
||||||
|
$extraOptions[] = '-d';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $extraOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name property of the object.
|
||||||
|
*
|
||||||
|
* This method assigns the provided name to the object's internal property
|
||||||
|
* and returns the current instance for method chaining.
|
||||||
|
*
|
||||||
|
* @param string $name The name to set.
|
||||||
|
* @return self The current instance of the object.
|
||||||
|
*/
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the working directory.
|
||||||
|
*
|
||||||
|
* This method assigns the provided directory path to the internal
|
||||||
|
* working directory property and returns the current instance for
|
||||||
|
* method chaining.
|
||||||
|
*
|
||||||
|
* @param string $workingDir The path to set as the working directory.
|
||||||
|
* @return self The current instance for fluent interface usage.
|
||||||
|
*/
|
||||||
|
public function setWorkingDir(string $workingDir): self
|
||||||
|
{
|
||||||
|
$this->workingDir = $workingDir;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout value for the start command.
|
||||||
|
*
|
||||||
|
* This method assigns a specified timeout value to the start command
|
||||||
|
* and returns the current instance for method chaining.
|
||||||
|
*
|
||||||
|
* @param float $startCommandTimeout The timeout duration in seconds.
|
||||||
|
* @return self The current instance with the updated timeout value.
|
||||||
|
*/
|
||||||
|
public function setStartCommandTimeout(float $startCommandTimeout): self
|
||||||
|
{
|
||||||
|
$this->startCommandTimeout = $startCommandTimeout;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Docker host configuration.
|
||||||
|
*
|
||||||
|
* This method assigns the provided Docker host value to the corresponding property
|
||||||
|
* and returns the current instance for method chaining.
|
||||||
|
*
|
||||||
|
* @param string $dockerHost The Docker host address to be set.
|
||||||
|
*
|
||||||
|
* @return self The current instance of the class.
|
||||||
|
*/
|
||||||
|
public function setDockerHost(string $dockerHost): self
|
||||||
|
{
|
||||||
|
$this->dockerHost = $dockerHost;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the daemonize mode for the application.
|
||||||
|
*
|
||||||
|
* This method allows enabling or disabling the daemon mode
|
||||||
|
* by setting a boolean value. The current instance of the class
|
||||||
|
* is returned to allow method chaining.
|
||||||
|
*
|
||||||
|
* @param bool $daemonize Indicates whether to enable or disable daemon mode.
|
||||||
|
* @return self The current instance of the class.
|
||||||
|
*/
|
||||||
|
public function daemonize(bool $daemonize): self
|
||||||
|
{
|
||||||
|
$this->daemonize = $daemonize;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets and merges Docker Compose files.
|
||||||
|
*
|
||||||
|
* This method updates the list of Docker Compose files by merging the
|
||||||
|
* provided array of files into the existing list. The operation ensures
|
||||||
|
* that the provided files are appended to the current set of files.
|
||||||
|
*
|
||||||
|
* @param array $composeFiles The array of Docker Compose file paths to add.
|
||||||
|
* @return self Returns the current instance for method chaining.
|
||||||
|
*/
|
||||||
|
public function setComposeFiles(array $composeFiles): self
|
||||||
|
{
|
||||||
|
$this->composeFiles = [...$this->composeFiles, ...$composeFiles];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the environment files and merges them with the existing ones.
|
||||||
|
*
|
||||||
|
* This method appends the provided array of environment file paths to the
|
||||||
|
* existing list of files stored in the class. The updated list of files
|
||||||
|
* is then returned for further operations.
|
||||||
|
*
|
||||||
|
* @param array $envFiles An array of environment file paths to be added.
|
||||||
|
* @return self The current instance for method chaining.
|
||||||
|
*/
|
||||||
|
public function setEnvFiles(array $envFiles): self
|
||||||
|
{
|
||||||
|
$this->envFiles = [...$this->envFiles, ...$envFiles];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the inline environment files and appends them to the existing list.
|
||||||
|
*
|
||||||
|
* This method updates the internal list of inline environment files by
|
||||||
|
* appending the provided array of file paths to the current list. It allows
|
||||||
|
* for dynamically adding environment file paths to be processed.
|
||||||
|
*
|
||||||
|
* @param array $envFiles An array of environment file paths to be added.
|
||||||
|
* @return self The current instance for method chaining.
|
||||||
|
*/
|
||||||
|
public function setInlineEnvFiles(array $envFiles): self
|
||||||
|
{
|
||||||
|
$this->inlineEnvFiles = [...$this->inlineEnvFiles, ...$envFiles];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of inline environment files.
|
||||||
|
*
|
||||||
|
* This method returns an array containing the names of the environment
|
||||||
|
* files that are processed inline. These files typically contain environment
|
||||||
|
* variables that can be used by the application.
|
||||||
|
*
|
||||||
|
* @return array The list of inline environment file names.
|
||||||
|
*/
|
||||||
|
public function getInlineEnvFiles(): array
|
||||||
|
{
|
||||||
|
return $this->inlineEnvFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the inline environment files and merges their contents.
|
||||||
|
*
|
||||||
|
* This method iterates through a list of inline environment files,
|
||||||
|
* reads their content, and parses them into an associative array.
|
||||||
|
* Any files that cannot be read or parsed will be skipped. The resulting
|
||||||
|
* data from all parsed files is combined into a single array and returned.
|
||||||
|
*
|
||||||
|
* @return array The merged environment variables from the parsed files.
|
||||||
|
*/
|
||||||
|
protected function parseInlineEnvFiles(): array
|
||||||
|
{
|
||||||
|
$env = [];
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
|
||||||
|
foreach ($this->inlineEnvFiles as $file)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(!empty($envFromFile = Dotenv::parse($filesystem->readFile($file))))
|
||||||
|
{
|
||||||
|
$env = [...$env, ...$envFromFile];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception $e)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads environment variables from the application's working directory.
|
||||||
|
*
|
||||||
|
* This method uses the Dotenv library to parse and retrieve environment
|
||||||
|
* variables defined in the designated working directory. If an error occurs
|
||||||
|
* during the loading process, an empty array is returned.
|
||||||
|
*
|
||||||
|
* @return array The environment variables loaded from the working directory.
|
||||||
|
*/
|
||||||
|
protected function loadDotEnv(): array
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Dotenv::createArrayBacked($this->workingDir)->load();
|
||||||
|
}
|
||||||
|
catch(Exception $e)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/Service/Git/GitDeployment.php
Normal file
154
src/Service/Git/GitDeployment.php
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* GitDeployment.php 2026-03-27 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the deployment process for the specified project by fetching the required
|
||||||
|
* resources and deploying the appropriate version.
|
||||||
|
*
|
||||||
|
* This method first fetches the necessary project resources. Then, it delegates
|
||||||
|
* the deployment process to the deployment method, optionally using the provided
|
||||||
|
* version and step parameters.
|
||||||
|
*
|
||||||
|
* @param Project $project The project to handle.
|
||||||
|
* @param string $version An optional specific version to deploy. Defaults to an empty string.
|
||||||
|
* @param string $step An optional step parameter used during the handling process. Defaults to an empty string.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(Project $project, string $version = '', string $step = ''): void
|
||||||
|
{
|
||||||
|
$this->fetch($project);
|
||||||
|
$this->deploy($project, $version);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the latest updates for the specified project's repository.
|
||||||
|
* Depending on the state of the repository directory, either a fetch or
|
||||||
|
* a full clone operation is performed. Supports authenticated access to
|
||||||
|
* repositories when a URL with HTTP is provided.
|
||||||
|
*
|
||||||
|
* If the repository is already cloned, this method performs a fetch operation
|
||||||
|
* to synchronize the local repository. Otherwise, it clones the repository in
|
||||||
|
* mirror mode to the designated directory.
|
||||||
|
*
|
||||||
|
* During the process, the deployment step is updated to reflect the current
|
||||||
|
* stage of the fetch operation for the given project.
|
||||||
|
*
|
||||||
|
* @param Project $project The project whose repository updates are to be fetched.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
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'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deploys the specified project by preparing a clean working directory,
|
||||||
|
* cloning the git repository, and checking out the appropriate version.
|
||||||
|
*
|
||||||
|
* If the version is set to "default", it translates to the current state of
|
||||||
|
* the default branch in the repository. If set to "latest" or if no version
|
||||||
|
* is provided, the latest release of the repository is checked out.
|
||||||
|
*
|
||||||
|
* @param Project $project The project to be deployed.
|
||||||
|
* @param string $version The version to be deployed, which could be explicit, "default", or "latest".
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/Service/Git/GitService.php
Normal file
225
src/Service/Git/GitService.php
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* GitService.php 2026-03-27 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides functionalities for managing Git repositories, including cloning,
|
||||||
|
* fetching, checking out, and retrieving information about branches and releases.
|
||||||
|
*/
|
||||||
|
class GitService
|
||||||
|
{
|
||||||
|
use LoggerTrait;
|
||||||
|
|
||||||
|
protected string $workingDir;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
){
|
||||||
|
$this->workingDir = sys_get_temp_dir();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the working directory to the specified path.
|
||||||
|
*
|
||||||
|
* @param string $dir The path to set as the working directory.
|
||||||
|
* @return self The current instance for method chaining.
|
||||||
|
*/
|
||||||
|
public function setWorkingDir(string $dir): self
|
||||||
|
{
|
||||||
|
$this->workingDir = $dir;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified directory within the working directory is a cloned Git repository.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory to check. Defaults to '.git'.
|
||||||
|
* @return bool True if the directory exists and is a cloned Git repository, false otherwise.
|
||||||
|
*/
|
||||||
|
public function isCloned(string $dir = '.git'): bool
|
||||||
|
{
|
||||||
|
return is_dir($this->workingDir . DIRECTORY_SEPARATOR . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones a Git repository into the specified target directory with optional configurations.
|
||||||
|
*
|
||||||
|
* @param string $repoUrl The URL of the Git repository to be cloned.
|
||||||
|
* @param string $targetDir The target directory where the repository will be cloned. Defaults to an empty string, leading to the use of the repository's default folder name.
|
||||||
|
* @param array $options Optional configurations for the cloning process. Supports 'auth_basic' for basic authentication with keys:
|
||||||
|
* - 'username': The username for authentication.
|
||||||
|
* - 'password': The password for authentication.
|
||||||
|
* Other options will be passed directly to the `git clone` command.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks out the specified branch in the Git repository located in the given directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory containing the Git repository.
|
||||||
|
* @param string $branch The name of the branch to check out. Defaults to 'master'.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function checkoutRepo(string $dir, string $branch = 'master'): void
|
||||||
|
{
|
||||||
|
new Process(
|
||||||
|
command: ['git', 'checkout', '-f', $branch],
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir,
|
||||||
|
)->mustRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all branches and tags from the Git repository within the specified directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory containing the Git repository.
|
||||||
|
* @param array $options Additional options for configuring the fetch process (currently unused).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function fetchRepo(string $dir, array $options = []): void
|
||||||
|
{
|
||||||
|
Process::fromShellCommandline(
|
||||||
|
command: 'git --all --tags',
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir,
|
||||||
|
)->mustRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Git references in the specified repository directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory containing the Git repository.
|
||||||
|
* @param array $options Additional options for the operation (not currently utilized).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function updateRef(string $dir, array $options = []): void
|
||||||
|
{
|
||||||
|
Process::fromShellCommandline(
|
||||||
|
command: 'git update-ref',
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir
|
||||||
|
)->mustRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a Git pull command to update all branches and fetch all tags in the specified repository directory.
|
||||||
|
*
|
||||||
|
* @param string $targetDir The relative path to the directory containing the Git repository.
|
||||||
|
* @param array $options An array of additional options (currently unused).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function pullRepo(string $targetDir, array $options = []): void
|
||||||
|
{
|
||||||
|
Process::fromShellCommandline(
|
||||||
|
command: 'git pull --all --tags --force',
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $targetDir
|
||||||
|
)->mustRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the default branch name of the Git repository within the specified directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory containing the Git repository.
|
||||||
|
* @return string The default branch name, or 'master' if none is found.
|
||||||
|
*/
|
||||||
|
public function getRepoDefaultBranch(string $dir): string
|
||||||
|
{
|
||||||
|
$process = Process::fromShellCommandline(
|
||||||
|
command: 'git symbolic-ref --short HEAD',
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir
|
||||||
|
)->mustRun();
|
||||||
|
|
||||||
|
return trim($process->getOutput()) ?: 'master';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current release tag from the Git repository within the specified directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory containing the Git repository.
|
||||||
|
* @return string The current release tag.
|
||||||
|
*/
|
||||||
|
public function getCurrentRelease(string $dir): string
|
||||||
|
{
|
||||||
|
$process = Process::fromShellCommandline(
|
||||||
|
command: 'git describe --tags',
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir
|
||||||
|
)->mustRun();
|
||||||
|
|
||||||
|
return trim($process->getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the latest release tag from the Git repository within the specified directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The relative path to the directory containing the Git repository.
|
||||||
|
* @return string The latest release tag.
|
||||||
|
*/
|
||||||
|
public function getLatestRelease(string $dir): string
|
||||||
|
{
|
||||||
|
$process = Process::fromShellCommandline(
|
||||||
|
command: 'git tag --sort=committerdate --list "v[0-9]*" "[0-9]*.[0-9]*.[0-9]*" | tail -1',
|
||||||
|
cwd: $this->workingDir . DIRECTORY_SEPARATOR . $dir
|
||||||
|
)->mustRun();
|
||||||
|
|
||||||
|
return trim($process->getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks out the latest release version in the specified directory.
|
||||||
|
*
|
||||||
|
* Compares the current release version with the latest available version.
|
||||||
|
* If the current version is already up to date, the update process is skipped.
|
||||||
|
* Otherwise, the latest version is downloaded and checked out.
|
||||||
|
*
|
||||||
|
* @param string $dir The directory where the release is located.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Service/Process/CleanProcess.php
Normal file
46
src/Service/Process/CleanProcess.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* CleanProcess.php 2026-03-27 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Service\Process;
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
class CleanProcess extends Process
|
||||||
|
{
|
||||||
|
public function start(?callable $callback = null, array $env = []): void
|
||||||
|
{
|
||||||
|
// von docker compose gesetzte Umgebungsvariablen zurücksetzen
|
||||||
|
$this->parseEnvVars('DOCKER_DOTENV_VARS', $env);
|
||||||
|
|
||||||
|
// Symfony dotenv-Variablen für den Prozess zurücksetzen
|
||||||
|
$this->parseEnvVars('SYMFONY_DOTENV_VARS', $env);
|
||||||
|
|
||||||
|
parent::start($callback, $env);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function parseEnvVars(string $keySelector, array &$env): void
|
||||||
|
{
|
||||||
|
$preservedKeys = [];
|
||||||
|
|
||||||
|
if(empty(getenv($keySelector)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare DDEV-Environments
|
||||||
|
if(getenv('IS_DDEV_PROJECT'))
|
||||||
|
{
|
||||||
|
$preservedKeys += ['SSH_AUTH_SOCK'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$vars = array_fill_keys(explode(',', getenv($keySelector)), false);
|
||||||
|
|
||||||
|
$env = [...array_diff_key($vars, array_flip($preservedKeys)), ...$env];
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/Service/ProjectConfigDirService.php
Normal file
125
src/Service/ProjectConfigDirService.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ProjectConfigDirService.php 2026-03-15 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Traits\LoggerTrait;
|
||||||
|
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\Filesystem\Path;
|
||||||
|
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
use Symfony\Component\Finder\SplFileInfo;
|
||||||
|
|
||||||
|
class ProjectConfigDirService
|
||||||
|
{
|
||||||
|
use LoggerTrait;
|
||||||
|
|
||||||
|
const string FILE_NAME_DEPLOYED_VERSION = '.deployed-version';
|
||||||
|
const string FILE_NAME_DEPLOYMENT_STEP = '.deployment-step';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected string $dataDir,
|
||||||
|
){}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SplFileInfo[]
|
||||||
|
*/
|
||||||
|
public function getProjectDirContents(): array
|
||||||
|
{
|
||||||
|
return $this->loadProjectDirContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getDeployedVersion(string $projectDir): string
|
||||||
|
{
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
if(!$filesystem->exists($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYED_VERSION))
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = $filesystem->readFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYED_VERSION);
|
||||||
|
|
||||||
|
return trim($version);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function setDeployedVersion(string $projectDir, string $version): void
|
||||||
|
{
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
$filesystem->dumpFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYED_VERSION, $version);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getDeploymentStep(string $projectDir): string
|
||||||
|
{
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
if(!$filesystem->exists($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYMENT_STEP))
|
||||||
|
{
|
||||||
|
return 'not-started';
|
||||||
|
}
|
||||||
|
|
||||||
|
$stage = $filesystem->readFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYMENT_STEP);
|
||||||
|
|
||||||
|
return trim($stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function setDeploymentStep(string $projectDir, string $step): void
|
||||||
|
{
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
$filesystem->dumpFile($projectDir . DIRECTORY_SEPARATOR . self::FILE_NAME_DEPLOYMENT_STEP, $step);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SplFileInfo[]
|
||||||
|
*/
|
||||||
|
protected function loadProjectDirContents(): array
|
||||||
|
{
|
||||||
|
$finder = new Finder();
|
||||||
|
$content = [];
|
||||||
|
|
||||||
|
// find all files in the current directory
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$finder->files()->in($this->dataDir)->depth(1)->name('project.yaml');
|
||||||
|
|
||||||
|
foreach($finder as $file)
|
||||||
|
{
|
||||||
|
$content[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
catch(DirectoryNotFoundException $e)
|
||||||
|
{
|
||||||
|
$this->logger->error('Project directory not found: ' . $this->dataDir . '. creating ...');
|
||||||
|
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$filesystem->mkdir(Path::normalize($this->dataDir));
|
||||||
|
$this->loadProjectDirContents();
|
||||||
|
}
|
||||||
|
catch(IOExceptionInterface $exception)
|
||||||
|
{
|
||||||
|
echo "An error occurred while creating your directory at ".$exception->getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(\Exception $e)
|
||||||
|
{
|
||||||
|
throw new \RuntimeException('Failed to load project directory contents', 500, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Traits/LoggerTrait.php
Normal file
23
src/Traits/LoggerTrait.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* LoggerTrait.php 2026-03-12 thomas
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Thomas Schneider <thomas@inter-mundos.de>
|
||||||
|
* Alle Rechte vorbehalten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Contracts\Service\Attribute\Required;
|
||||||
|
|
||||||
|
trait LoggerTrait
|
||||||
|
{
|
||||||
|
protected LoggerInterface $logger;
|
||||||
|
|
||||||
|
#[Required]
|
||||||
|
public function setLogger(LoggerInterface $logger): void
|
||||||
|
{
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user