feat: add script mappings sync command
This commit is contained in:
parent
e3517c3fcd
commit
e53b4ca85a
2 changed files with 194 additions and 0 deletions
55
src/Command/SyncScriptMappingsCommand.php
Normal file
55
src/Command/SyncScriptMappingsCommand.php
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Exception\GitSyncFailedException;
|
||||||
|
use App\Repository\ScriptMappingRepository;
|
||||||
|
use App\Service\GitSynchronizer;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(name: 'app:mappings:sync', description: 'Synchronize all script mappings.')]
|
||||||
|
final class SyncScriptMappingsCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ScriptMappingRepository $scriptMappingRepository,
|
||||||
|
private readonly GitSynchronizer $gitSynchronizer,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$synchronized = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($this->scriptMappingRepository->findAllOrderedByPublicPath() as $mapping) {
|
||||||
|
try {
|
||||||
|
$this->gitSynchronizer->sync($mapping);
|
||||||
|
++$synchronized;
|
||||||
|
} catch (GitSyncFailedException) {
|
||||||
|
++$failed;
|
||||||
|
$io->error(sprintf('Mapping "%s" synchronization failed.', $mapping->getPublicPath()));
|
||||||
|
} finally {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = sprintf('%d mapping(s) synchronized, %d failed.', $synchronized, $failed);
|
||||||
|
if ($failed === 0) {
|
||||||
|
$io->success($summary);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->warning($summary);
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
139
tests/Command/SyncScriptMappingsCommandTest.php
Normal file
139
tests/Command/SyncScriptMappingsCommandTest.php
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Command;
|
||||||
|
|
||||||
|
use App\Entity\ScriptMapping;
|
||||||
|
use App\Tests\DatabaseWebTestCase;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
final class SyncScriptMappingsCommandTest extends DatabaseWebTestCase
|
||||||
|
{
|
||||||
|
private Filesystem $filesystem;
|
||||||
|
private string $workDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->filesystem = new Filesystem();
|
||||||
|
$this->workDir = sys_get_temp_dir().'/get-installer-bootstrap-command-test-'.bin2hex(random_bytes(6));
|
||||||
|
$this->filesystem->mkdir($this->workDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->filesystem->remove($this->workDir);
|
||||||
|
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCommandSynchronizesEveryMapping(): void
|
||||||
|
{
|
||||||
|
$firstRepositoryDir = $this->createRepository('first', [
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\nprintf 'first'\n",
|
||||||
|
]);
|
||||||
|
$secondRepositoryDir = $this->createRepository('second', [
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\nprintf 'second'\n",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$firstMapping = $this->mapping('mcp/first/install.sh', $firstRepositoryDir);
|
||||||
|
$secondMapping = $this->mapping('mcp/second/install.sh', $secondRepositoryDir);
|
||||||
|
$this->entityManager->persist($firstMapping);
|
||||||
|
$this->entityManager->persist($secondMapping);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$application = new Application(static::$kernel);
|
||||||
|
self::assertTrue($application->has('app:mappings:sync'));
|
||||||
|
|
||||||
|
$commandTester = new CommandTester($application->find('app:mappings:sync'));
|
||||||
|
$commandTester->execute([]);
|
||||||
|
|
||||||
|
self::assertSame(0, $commandTester->getStatusCode());
|
||||||
|
self::assertStringContainsString('2 mapping(s) synchronized, 0 failed.', $commandTester->getDisplay());
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$syncedFirstMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($firstMapping->getId());
|
||||||
|
$syncedSecondMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($secondMapping->getId());
|
||||||
|
|
||||||
|
self::assertInstanceOf(ScriptMapping::class, $syncedFirstMapping);
|
||||||
|
self::assertInstanceOf(ScriptMapping::class, $syncedSecondMapping);
|
||||||
|
self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $syncedFirstMapping->getLastSyncStatus());
|
||||||
|
self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $syncedSecondMapping->getLastSyncStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCommandContinuesAfterMappingFailureAndReturnsFailure(): void
|
||||||
|
{
|
||||||
|
$failingRepositoryDir = $this->createRepository('failing', [
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\nprintf 'missing target'\n",
|
||||||
|
]);
|
||||||
|
$workingRepositoryDir = $this->createRepository('working', [
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\nprintf 'working'\n",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$failingMapping = $this->mapping('mcp/a-failing/install.sh', $failingRepositoryDir)
|
||||||
|
->setRepositoryFilePath('missing.sh');
|
||||||
|
$workingMapping = $this->mapping('mcp/b-working/install.sh', $workingRepositoryDir);
|
||||||
|
$this->entityManager->persist($failingMapping);
|
||||||
|
$this->entityManager->persist($workingMapping);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$application = new Application(static::$kernel);
|
||||||
|
self::assertTrue($application->has('app:mappings:sync'));
|
||||||
|
|
||||||
|
$commandTester = new CommandTester($application->find('app:mappings:sync'));
|
||||||
|
$commandTester->execute([]);
|
||||||
|
|
||||||
|
self::assertSame(1, $commandTester->getStatusCode());
|
||||||
|
self::assertStringContainsString('1 mapping(s) synchronized, 1 failed.', $commandTester->getDisplay());
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$syncedFailingMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($failingMapping->getId());
|
||||||
|
$syncedWorkingMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($workingMapping->getId());
|
||||||
|
|
||||||
|
self::assertInstanceOf(ScriptMapping::class, $syncedFailingMapping);
|
||||||
|
self::assertInstanceOf(ScriptMapping::class, $syncedWorkingMapping);
|
||||||
|
self::assertSame(ScriptMapping::SYNC_STATUS_FAILED, $syncedFailingMapping->getLastSyncStatus());
|
||||||
|
self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $syncedWorkingMapping->getLastSyncStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapping(string $publicPath, string $repositoryDir): ScriptMapping
|
||||||
|
{
|
||||||
|
return (new ScriptMapping())
|
||||||
|
->setPublicPath($publicPath)
|
||||||
|
->setRepositoryUrl($repositoryDir)
|
||||||
|
->setGitRef('main')
|
||||||
|
->setRepositoryFilePath('install.sh')
|
||||||
|
->setActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $files */
|
||||||
|
private function createRepository(string $name, array $files): string
|
||||||
|
{
|
||||||
|
$repositoryDir = $this->workDir.'/'.$name;
|
||||||
|
$this->filesystem->mkdir($repositoryDir);
|
||||||
|
|
||||||
|
foreach ($files as $path => $content) {
|
||||||
|
$fullPath = $repositoryDir.'/'.$path;
|
||||||
|
$this->filesystem->mkdir(dirname($fullPath));
|
||||||
|
file_put_contents($fullPath, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->runGit(['init'], $repositoryDir);
|
||||||
|
$this->runGit(['config', 'user.email', 'tests@example.com'], $repositoryDir);
|
||||||
|
$this->runGit(['config', 'user.name', 'Tests'], $repositoryDir);
|
||||||
|
$this->runGit(['add', '.'], $repositoryDir);
|
||||||
|
$this->runGit(['commit', '-m', 'Initial commit'], $repositoryDir);
|
||||||
|
$this->runGit(['branch', '-M', 'main'], $repositoryDir);
|
||||||
|
|
||||||
|
return $repositoryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $arguments */
|
||||||
|
private function runGit(array $arguments, string $cwd): void
|
||||||
|
{
|
||||||
|
(new Process(['git', ...$arguments], $cwd))->mustRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue