diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 1f6d082..5f978ed 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -133,8 +133,15 @@ jobs: sudo -u mysql mysqld --datadir=/var/lib/mysql --default-time-zone=SYSTEM --initialize-insecure sudo systemctl start mysql + - name: Install shopware-cli + uses: shopware/shopware-cli-action@v2 + - name: Create new Shopware Project - run: composer create-project shopware/production:6.5.8.8 . --no-interaction + run: | + shopware-cli project create shop 6.5.8.8 --no-audit + mv shop/* . + mv shop/.* . || true + rm -rf shop - name: Checkout uses: actions/checkout@v4 diff --git a/src/Application.php b/src/Application.php index 79435bc..6208746 100644 --- a/src/Application.php +++ b/src/Application.php @@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\XmlDumper; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\Filesystem\Filesystem; @@ -61,8 +61,8 @@ private function createContainer(): ContainerBuilder InstalledVersions::reload(include $projectDir . '/vendor/composer/installed.php'); (new DotenvLoader($projectDir))->load(); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/Resources/config')); - $loader->load('services.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/Resources/config')); + $loader->load('services.php'); $container->compile(); $container->set(self::class, $this); diff --git a/src/Config/ConfigFactory.php b/src/Config/ConfigFactory.php index d880e49..a76e46b 100644 --- a/src/Config/ConfigFactory.php +++ b/src/Config/ConfigFactory.php @@ -6,6 +6,8 @@ use Shopware\Deployment\Application; use Shopware\Deployment\Helper\EnvironmentHelper; +use Shopware\Deployment\Struct\OneTimeTask; +use Shopware\Deployment\Struct\OneTimeTaskWhen; use Symfony\Component\Filesystem\Path; use Symfony\Component\Yaml\Yaml; @@ -76,7 +78,16 @@ private static function fillConfig(ProjectConfiguration $projectConfiguration, a if (isset($deployment['one-time-tasks']) && \is_array($deployment['one-time-tasks'])) { foreach ($deployment['one-time-tasks'] as $task) { if (isset($task['id'], $task['script']) && \is_string($task['id']) && \is_string($task['script'])) { - $projectConfiguration->oneTimeTasks[$task['id']] = $task['script']; + $when = OneTimeTaskWhen::AFTER; + if (isset($task['when']) && \is_string($task['when'])) { + $when = OneTimeTaskWhen::from($task['when']); + } + + $projectConfiguration->oneTimeTasks[$task['id']] = new OneTimeTask( + $task['id'], + $task['script'], + $when + ); } } } diff --git a/src/Config/ProjectConfiguration.php b/src/Config/ProjectConfiguration.php index 5579067..e2eea77 100644 --- a/src/Config/ProjectConfiguration.php +++ b/src/Config/ProjectConfiguration.php @@ -15,7 +15,7 @@ class ProjectConfiguration public ProjectStore $store; /** - * @var array + * @var array */ public array $oneTimeTasks = []; diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php new file mode 100644 index 0000000..4ec36ca --- /dev/null +++ b/src/Resources/config/services.php @@ -0,0 +1,39 @@ +services() + ->defaults() + ->autowire() + ->autoconfigure() + ->public(); + + $services->set('event_dispatcher', Symfony\Component\EventDispatcher\EventDispatcher::class); + $services->alias(EventDispatcherInterface::class, 'event_dispatcher'); + + $services->set(Doctrine\DBAL\Connection::class) + ->factory([MySQLFactory::class, 'createAndRetry']); + + $services->load('Shopware\\Deployment\\', '../../') + ->exclude('../../{Application.php,ApplicationOutput.php,Struct,Resources}'); + + $services->set(Application::class) + ->synthetic(); + + $services->set(ProjectConfiguration::class) + ->factory([ConfigFactory::class, 'create']) + ->args([ + '%kernel.project_dir%', + service(Application::class), + ]); +}; diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml deleted file mode 100644 index c291018..0000000 --- a/src/Resources/config/services.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - %kernel.project_dir% - - - - diff --git a/src/Services/OneTimeTasks.php b/src/Services/OneTimeTasks.php index 44986e5..1f13b7e 100644 --- a/src/Services/OneTimeTasks.php +++ b/src/Services/OneTimeTasks.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Connection; use Shopware\Deployment\Config\ProjectConfiguration; use Shopware\Deployment\Helper\ProcessHelper; +use Shopware\Deployment\Struct\OneTimeTaskWhen; use Symfony\Component\Console\Output\OutputInterface; class OneTimeTasks @@ -18,18 +19,22 @@ public function __construct( ) { } - public function execute(OutputInterface $output): void + public function execute(OutputInterface $output, OneTimeTaskWhen $when): void { $executed = $this->getExecutedTasks(); - foreach ($this->configuration->oneTimeTasks as $id => $script) { + foreach ($this->configuration->oneTimeTasks as $id => $task) { + if ($task->when !== $when) { + continue; + } + if (isset($executed[$id])) { continue; } $output->writeln('Running one-time task ' . $id); - $this->processHelper->runAndTail($script); + $this->processHelper->runAndTail($task->script); $this->markAsRun($id); } diff --git a/src/Services/UpgradeManager.php b/src/Services/UpgradeManager.php index be762a7..a3fb22c 100644 --- a/src/Services/UpgradeManager.php +++ b/src/Services/UpgradeManager.php @@ -7,6 +7,7 @@ use Shopware\Deployment\Config\ProjectConfiguration; use Shopware\Deployment\Helper\EnvironmentHelper; use Shopware\Deployment\Helper\ProcessHelper; +use Shopware\Deployment\Struct\OneTimeTaskWhen; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\OutputInterface; @@ -32,6 +33,9 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->hookExecutor->execute(HookExecutor::HOOK_PRE_UPDATE); + // Execute one-time tasks that should run before the update + $this->oneTimeTasks->execute($output, OneTimeTaskWhen::BEFORE); + if ($this->configuration->maintenance->enabled) { $this->state->enableMaintenanceMode(); @@ -89,7 +93,8 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->processHelper->console(['theme:compile', '--active-only']); } - $this->oneTimeTasks->execute($output); + // Execute one-time tasks that should run after the update + $this->oneTimeTasks->execute($output, OneTimeTaskWhen::AFTER); $this->hookExecutor->execute(HookExecutor::HOOK_POST_UPDATE); diff --git a/src/Struct/OneTimeTask.php b/src/Struct/OneTimeTask.php new file mode 100644 index 0000000..3da188a --- /dev/null +++ b/src/Struct/OneTimeTask.php @@ -0,0 +1,15 @@ +createMockApplication()); static::assertTrue($config->extensionManagement->enabled); static::assertSame('ignore', $config->extensionManagement->overrides['Name']['state']); - static::assertSame(['foo' => 'test'], $config->oneTimeTasks); + static::assertArrayHasKey('foo', $config->oneTimeTasks); + static::assertInstanceOf(\Shopware\Deployment\Struct\OneTimeTask::class, $config->oneTimeTasks['foo']); + static::assertSame('foo', $config->oneTimeTasks['foo']->id); + static::assertSame('test', $config->oneTimeTasks['foo']->script); + static::assertSame(OneTimeTaskWhen::AFTER, $config->oneTimeTasks['foo']->when); static::assertNotSame('', $config->hooks->pre); static::assertNotSame('', $config->hooks->post); static::assertNotSame('', $config->hooks->preInstall); @@ -132,7 +137,9 @@ public function testCreateWithProjectConfigOption(): void $config = ConfigFactory::create(__DIR__, $this->createMockApplication($customConfigPath)); static::assertTrue($config->extensionManagement->enabled); - static::assertSame(['foo' => 'test'], $config->oneTimeTasks); + static::assertArrayHasKey('foo', $config->oneTimeTasks); + static::assertInstanceOf(\Shopware\Deployment\Struct\OneTimeTask::class, $config->oneTimeTasks['foo']); + static::assertSame('test', $config->oneTimeTasks['foo']->script); } public function testCreateWithProjectConfigOptionRelativePath(): void @@ -141,7 +148,9 @@ public function testCreateWithProjectConfigOptionRelativePath(): void $config = ConfigFactory::create(__DIR__ . '/_fixtures', $this->createMockApplication('yml/.shopware-project.yml')); static::assertTrue($config->extensionManagement->enabled); - static::assertSame(['foo' => 'test'], $config->oneTimeTasks); + static::assertArrayHasKey('foo', $config->oneTimeTasks); + static::assertInstanceOf(\Shopware\Deployment\Struct\OneTimeTask::class, $config->oneTimeTasks['foo']); + static::assertSame('test', $config->oneTimeTasks['foo']->script); } #[Env('SHOPWARE_PROJECT_CONFIG_FILE', '_fixtures/yml/.shopware-project.yml')] @@ -151,7 +160,9 @@ public function testEnvironmentVariableOverridesProjectConfigOption(): void $config = ConfigFactory::create(__DIR__, $this->createMockApplication('some-other-config.yml')); // Should load the config from environment variable, not the CLI option - static::assertSame(['foo' => 'test'], $config->oneTimeTasks); + static::assertArrayHasKey('foo', $config->oneTimeTasks); + static::assertInstanceOf(\Shopware\Deployment\Struct\OneTimeTask::class, $config->oneTimeTasks['foo']); + static::assertSame('test', $config->oneTimeTasks['foo']->script); } public function testCreateWithNonExistentProjectConfig(): void @@ -164,4 +175,18 @@ public function testCreateWithNonExistentProjectConfig(): void static::assertSame([], $config->extensionManagement->overrides); static::assertSame([], $config->oneTimeTasks); } + + public function testOneTimeTasksWithWhenField(): void + { + $config = ConfigFactory::create(__DIR__ . '/_fixtures/maintenance-mode', $this->createMockApplication()); + + static::assertArrayHasKey('foo', $config->oneTimeTasks); + static::assertSame(OneTimeTaskWhen::AFTER, $config->oneTimeTasks['foo']->when); + + static::assertArrayHasKey('early-task', $config->oneTimeTasks); + static::assertSame(OneTimeTaskWhen::BEFORE, $config->oneTimeTasks['early-task']->when); + + static::assertArrayHasKey('late-task', $config->oneTimeTasks); + static::assertSame(OneTimeTaskWhen::AFTER, $config->oneTimeTasks['late-task']->when); + } } diff --git a/tests/Config/_fixtures/maintenance-mode/.shopware-project.yml b/tests/Config/_fixtures/maintenance-mode/.shopware-project.yml index 3d18666..42c0583 100644 --- a/tests/Config/_fixtures/maintenance-mode/.shopware-project.yml +++ b/tests/Config/_fixtures/maintenance-mode/.shopware-project.yml @@ -26,3 +26,9 @@ deployment: one-time-tasks: - id: foo script: test + - id: early-task + script: echo "Running early" + when: before + - id: late-task + script: echo "Running late" + when: after diff --git a/tests/Services/OneTimeTasksTest.php b/tests/Services/OneTimeTasksTest.php index 8ed91a1..fc4ec46 100644 --- a/tests/Services/OneTimeTasksTest.php +++ b/tests/Services/OneTimeTasksTest.php @@ -10,6 +10,8 @@ use Shopware\Deployment\Config\ProjectConfiguration; use Shopware\Deployment\Helper\ProcessHelper; use Shopware\Deployment\Services\OneTimeTasks; +use Shopware\Deployment\Struct\OneTimeTask; +use Shopware\Deployment\Struct\OneTimeTaskWhen; use Symfony\Component\Console\Output\OutputInterface; #[CoversClass(OneTimeTasks::class)] @@ -29,7 +31,7 @@ public function testNoTasks(): void $configuration = new ProjectConfiguration(); $tasks = new OneTimeTasks($processHelper, $connection, $configuration); - $tasks->execute($output); + $tasks->execute($output, OneTimeTaskWhen::AFTER); } public function testNoTasksNoTable(): void @@ -47,7 +49,7 @@ public function testNoTasksNoTable(): void $configuration = new ProjectConfiguration(); $tasks = new OneTimeTasks($processHelper, $connection, $configuration); - $tasks->execute($output); + $tasks->execute($output, OneTimeTaskWhen::BEFORE); } public function testTask(): void @@ -68,11 +70,11 @@ public function testTask(): void $configuration = new ProjectConfiguration(); $configuration->oneTimeTasks = [ - 'test' => 'echo "test"', + 'test' => new OneTimeTask('test', 'echo "test"', OneTimeTaskWhen::AFTER), ]; $tasks = new OneTimeTasks($processHelper, $connection, $configuration); - $tasks->execute($output); + $tasks->execute($output, OneTimeTaskWhen::AFTER); } public function testTaskAlreadyExecuted(): void @@ -88,11 +90,11 @@ public function testTaskAlreadyExecuted(): void $configuration = new ProjectConfiguration(); $configuration->oneTimeTasks = [ - 'test' => 'echo "test"', + 'test' => new OneTimeTask('test', 'echo "test"', OneTimeTaskWhen::AFTER), ]; $tasks = new OneTimeTasks($processHelper, $connection, $configuration); - $tasks->execute($output); + $tasks->execute($output, OneTimeTaskWhen::AFTER); } public function testRemove(): void @@ -105,4 +107,70 @@ public function testRemove(): void $tasks = new OneTimeTasks($processHelper, $connection, new ProjectConfiguration()); $tasks->remove('test'); } + + public function testTaskWithWhenBefore(): void + { + $output = $this->createMock(OutputInterface::class); + $output->expects($this->once())->method('writeln')->with('Running one-time task before-task'); + + $processHelper = $this->createMock(ProcessHelper::class); + $processHelper->expects($this->once())->method('runAndTail')->with('echo "before"'); + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('fetchAllAssociativeIndexed')->willReturn([]); + $connection->expects($this->once())->method('executeStatement'); + + $configuration = new ProjectConfiguration(); + $configuration->oneTimeTasks = [ + 'before-task' => new OneTimeTask('before-task', 'echo "before"', OneTimeTaskWhen::BEFORE), + 'after-task' => new OneTimeTask('after-task', 'echo "after"', OneTimeTaskWhen::AFTER), + ]; + + $tasks = new OneTimeTasks($processHelper, $connection, $configuration); + $tasks->execute($output, OneTimeTaskWhen::BEFORE); + } + + public function testTaskWithWhenAfter(): void + { + $output = $this->createMock(OutputInterface::class); + $output->expects($this->once())->method('writeln')->with('Running one-time task after-task'); + + $processHelper = $this->createMock(ProcessHelper::class); + $processHelper->expects($this->once())->method('runAndTail')->with('echo "after"'); + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('fetchAllAssociativeIndexed')->willReturn([]); + $connection->expects($this->once())->method('executeStatement'); + + $configuration = new ProjectConfiguration(); + $configuration->oneTimeTasks = [ + 'before-task' => new OneTimeTask('before-task', 'echo "before"', OneTimeTaskWhen::BEFORE), + 'after-task' => new OneTimeTask('after-task', 'echo "after"', OneTimeTaskWhen::AFTER), + ]; + + $tasks = new OneTimeTasks($processHelper, $connection, $configuration); + $tasks->execute($output, OneTimeTaskWhen::AFTER); + } + + public function testTaskWithWhenFilterExecutesOnlyMatchingTasks(): void + { + $output = $this->createMock(OutputInterface::class); + $output->expects($this->exactly(1))->method('writeln'); + + $processHelper = $this->createMock(ProcessHelper::class); + $processHelper->expects($this->exactly(1))->method('runAndTail'); + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('fetchAllAssociativeIndexed')->willReturn([]); + $connection->expects($this->exactly(1))->method('executeStatement'); + + $configuration = new ProjectConfiguration(); + $configuration->oneTimeTasks = [ + 'before-task' => new OneTimeTask('before-task', 'echo "before"', OneTimeTaskWhen::BEFORE), + 'after-task' => new OneTimeTask('after-task', 'echo "after"', OneTimeTaskWhen::AFTER), + ]; + + $tasks = new OneTimeTasks($processHelper, $connection, $configuration); + $tasks->execute($output, OneTimeTaskWhen::BEFORE); + } } diff --git a/tests/Services/UpgradeManagerTest.php b/tests/Services/UpgradeManagerTest.php index 34a9c2d..244a3fa 100644 --- a/tests/Services/UpgradeManagerTest.php +++ b/tests/Services/UpgradeManagerTest.php @@ -27,7 +27,7 @@ public function testRun(): void { $oneTimeTasks = $this->createMock(OneTimeTasks::class); $oneTimeTasks - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('execute'); $hookExecutor = $this->createMock(HookExecutor::class); @@ -219,7 +219,7 @@ public function testRunWithLicenseDomain(): void { $oneTimeTasks = $this->createMock(OneTimeTasks::class); $oneTimeTasks - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('execute'); $hookExecutor = $this->createMock(HookExecutor::class);