From 6f141dc29044f423158ece92a9a78d8e1b017775 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Tue, 19 Aug 2025 13:30:19 +0000 Subject: [PATCH 01/17] feat: add tracking --- .devcontainer.json | 36 +++++++ src/Command/RunCommand.php | 10 ++ src/Services/InstallationManager.php | 5 + src/Services/ShopwareState.php | 46 +++++++++ src/Services/TrackingService.php | 110 +++++++++++++++++++++ src/Services/UpgradeManager.php | 21 +++- tests/Command/RunCommandTest.php | 64 +++++++++++- tests/Services/InstallationManagerTest.php | 6 ++ tests/Services/ShopwareStateTest.php | 50 ++++++++++ tests/Services/UpgradeManagerTest.php | 15 ++- 10 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 .devcontainer.json create mode 100644 src/Services/TrackingService.php diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..85f9e36 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,36 @@ +{ + "image": "ghcr.io/shopwarelabs/devcontainer/symfony-flex:6.6.10-8.3", + "overrideCommand": false, + "updateRemoteUserUID": false, + "forwardPorts": [ + 8000 + ], + "features": { + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {} + }, + "portsAttributes": { + "8000": { + "label": "Shopware", + "onAutoForward": "notify" + }, + "8080": { + "label": "Administration Watcher", + "onAutoForward": "notify" + } + }, + "containerEnv": { + "PROJECT_ROOT": "/var/www/html" + }, + "mounts": [ + "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", + "source=claude-code-config-${devcontainerId},target=/home/www-data/.claude,type=volume" + ], + "customizations": { + "vscode": { + "extensions": [ + "DEVSENSE.phptools-vscode", + "redhat.vscode-yaml" + ] + } + } +} \ No newline at end of file diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 0eecf11..c10ea84 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -9,6 +9,7 @@ use Shopware\Deployment\Services\HookExecutor; use Shopware\Deployment\Services\InstallationManager; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Services\UpgradeManager; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Attribute\AsCommand; @@ -27,6 +28,7 @@ public function __construct( private readonly UpgradeManager $upgradeManager, private readonly HookExecutor $hookExecutor, private readonly EventDispatcherInterface $eventDispatcher, + private readonly TrackingService $trackingService, ) { parent::__construct(); } @@ -41,6 +43,14 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + $this->trackingService->track('php_version', [ + 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, + ]); + + $this->trackingService->track('mysql_version', [ + 'mysql_version' => $this->state->getMySqlVersion(), + ]); + $timeout = $input->getOption('timeout'); $config = new RunConfiguration( diff --git a/src/Services/InstallationManager.php b/src/Services/InstallationManager.php index 6a7ad18..a880083 100644 --- a/src/Services/InstallationManager.php +++ b/src/Services/InstallationManager.php @@ -24,6 +24,7 @@ public function __construct( private readonly HookExecutor $hookExecutor, private readonly ProjectConfiguration $configuration, private readonly AccountService $accountService, + private readonly TrackingService $trackingService, ) { } @@ -56,7 +57,11 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $additionalInstallParameters[] = '--drop-database'; } + $took = microtime(true); $this->processHelper->console(['system:install', '--create-database', '--shop-locale=' . $shopLocale, '--shop-currency=' . $shopCurrency, '--force', ...$additionalInstallParameters]); + + $this->trackingService->track('installed', ['took' => microtime(true) - $took]); + $this->processHelper->console(['user:create', $adminUser, '--password=' . $adminPassword]); $this->processHelper->console(['messenger:setup-transports']); diff --git a/src/Services/ShopwareState.php b/src/Services/ShopwareState.php index e8628bb..3425830 100644 --- a/src/Services/ShopwareState.php +++ b/src/Services/ShopwareState.php @@ -100,4 +100,50 @@ public function disableMaintenanceMode(): void $this->connection->executeStatement('UPDATE sales_channel SET maintenance = ? WHERE id = UNHEX(?)', [$maintenance, $id]); } } + + public function getMySqlVersion(): string + { + $version = $this->extractMySQLVersion($this->connection->fetchOne('SELECT VERSION()')); + + if (isset($version['mariadb'])) { + return 'mariadb-' . $version['mariadb']; + } + + if (isset($version['mysql'])) { + return 'mysql-' . $version['mysql']; + } + + return 'unknown'; + } + + /** + * @return array{mysql?: string, mariadb?: string} + */ + private function extractMySQLVersion(string $versionString): array + { + if (stripos($versionString, 'mariadb') === false) { + $pos = strpos($versionString, '-'); + if (\is_int($pos)) { + $versionString = substr($versionString, 0, $pos); + } + + return ['mysql' => $versionString]; + } + + return ['mariadb' => self::getVersionNumber($versionString)]; + } + + private static function getVersionNumber(string $versionString): string + { + $match = preg_match( + '/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', + $versionString, + $versionParts, + ); + if ($match === 0 || \is_bool($match)) { + throw new \RuntimeException(\sprintf('Invalid version string: %s', $versionString)); + } + + return $versionParts['major'] . '.' . $versionParts['minor']; + } } diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php new file mode 100644 index 0000000..fd758ed --- /dev/null +++ b/src/Services/TrackingService.php @@ -0,0 +1,110 @@ + + */ + private array $defaultTags; + + private string $id; + + private HttpClientInterface $client; + + /** + * @var list<\Symfony\Contracts\HttpClient\ResponseInterface> + */ + private array $responses; + + public function __construct( + private readonly SystemConfigHelper $systemConfigHelper, + private readonly ShopwareState $shopwareState, + ) { + $this->client = HttpClient::create([ + 'base_uri' => EnvironmentHelper::getVariable('SHOPWARE_TRACKING_ENDPOINT', self::API_ENDPOINT), + ]); + + register_shutdown_function([$this, 'shutdown']); + } + + /** + * @param array $tags + */ + public function track(string $eventName, array $tags = []): void + { + if (EnvironmentHelper::hasVariable('DO_NOT_TRACK')) { + return; + } + + $tags += $this->getTags(); + $id = $this->getId(); + + // The variable is unused on purpose, otherwise __destruct on Response object is directly called, which makes this request synchronously + $this->responses[] = $this->client->request('PUT', '/track', [ + 'json' => [ + 'event' => 'deployment_helper.' . $eventName, + 'tags' => $tags, + 'user_id' => $id, + 'timestamp' => (new \DateTime())->format(\DateTime::ISO8601), + ], + 'timeout' => 0.8, + 'max_duration' => 0.8, + 'headers' => [ + 'User-Agent' => 'shopware-deployment-helper', + 'Accept' => 'application/json', + ], + ]); + } + + /** + * @return array + */ + private function getTags(): array + { + if (isset($this->defaultTags)) { + return $this->defaultTags; + } + + $this->defaultTags = [ + 'shopware_version' => $this->shopwareState->getCurrentVersion(), + ]; + + return $this->defaultTags; + } + + private function getId(): string + { + if (isset($this->id)) { + return $this->id; + } + + $id = $this->systemConfigHelper->get(self::DEPLOYMENT_HELPER_ID); + + if ($id === null) { + $id = bin2hex(random_bytes(16)); + $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $id); + } + + return $this->id = $id; + } + + private function shutdown(): void + { + usleep(100); + foreach ($this->responses as $response) { + $response->cancel(); + } + } +} diff --git a/src/Services/UpgradeManager.php b/src/Services/UpgradeManager.php index a3fb22c..03a058f 100644 --- a/src/Services/UpgradeManager.php +++ b/src/Services/UpgradeManager.php @@ -24,6 +24,7 @@ public function __construct( private readonly OneTimeTasks $oneTimeTasks, private readonly ProjectConfiguration $configuration, private readonly AccountService $accountService, + private readonly TrackingService $trackingService, ) { } @@ -47,8 +48,10 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->processHelper->console(['messenger:setup-transports']); - if ($this->state->getPreviousVersion() !== $this->state->getCurrentVersion()) { - $output->writeln(\sprintf('Updating Shopware from %s to %s', $this->state->getPreviousVersion(), $this->state->getCurrentVersion())); + $previousVersion = $this->state->getPreviousVersion(); + $currentVersion = $this->state->getCurrentVersion(); + if ($previousVersion !== $currentVersion) { + $output->writeln(\sprintf('Updating Shopware from %s to %s', $previousVersion, $currentVersion)); $additionalUpdateParameters = []; @@ -56,8 +59,16 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $additionalUpdateParameters[] = '--skip-asset-build'; } + $took = microtime(true); + $this->processHelper->console(['system:update:finish', ...$additionalUpdateParameters]); - $this->state->setVersion($this->state->getCurrentVersion()); + + $this->state->setVersion($currentVersion); + + $this->trackingService->track('upgrade', [ + 'took' => microtime(true) - $took, + 'previous_shopware_version' => $previousVersion, + ]); } $salesChannelUrl = EnvironmentHelper::getVariable('SALES_CHANNEL_URL'); @@ -81,7 +92,7 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->pluginHelper->removePlugins($configuration->skipAssetsInstall); if ($this->configuration->store->licenseDomain !== '') { - $this->accountService->refresh(new SymfonyStyle(new ArgvInput([]), $output), $this->state->getCurrentVersion(), $this->configuration->store->licenseDomain); + $this->accountService->refresh(new SymfonyStyle(new ArgvInput([]), $output), $currentVersion, $this->configuration->store->licenseDomain); } $this->appHelper->installApps(); @@ -90,7 +101,9 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->appHelper->removeApps(); if (!$configuration->skipThemeCompile) { + $took = microtime(true); $this->processHelper->console(['theme:compile', '--active-only']); + $this->trackingService->track('theme_compiled', ['took' => microtime(true) - $took]); } // Execute one-time tasks that should run after the update diff --git a/tests/Command/RunCommandTest.php b/tests/Command/RunCommandTest.php index f322173..7865df3 100644 --- a/tests/Command/RunCommandTest.php +++ b/tests/Command/RunCommandTest.php @@ -10,6 +10,7 @@ use Shopware\Deployment\Services\HookExecutor; use Shopware\Deployment\Services\InstallationManager; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Services\UpgradeManager; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -25,6 +26,10 @@ public function testInstall(): void ->expects($this->once()) ->method('isInstalled') ->willReturn(false); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('8.0.0'); $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor @@ -43,12 +48,27 @@ public function testInstall(): void return true; })); + $trackingService = $this->createMock(TrackingService::class); + $trackingService + ->expects($this->exactly(2)) + ->method('track') + ->willReturnCallback(function ($event, $data): void { + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('8.0.0', $data['mysql_version']); + } + }); + $command = new RunCommand( $state, $installationManager, $this->createMock(UpgradeManager::class), $hookExecutor, - new EventDispatcher() + new EventDispatcher(), + $trackingService ); $tester = new CommandTester($command); @@ -65,6 +85,10 @@ public function testUpdate(): void ->expects($this->once()) ->method('isInstalled') ->willReturn(true); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('5.7.0'); $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor @@ -88,12 +112,27 @@ public function testUpdate(): void return true; })); + $trackingService = $this->createMock(TrackingService::class); + $trackingService + ->expects($this->exactly(2)) + ->method('track') + ->willReturnCallback(function ($event, $data): void { + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('5.7.0', $data['mysql_version']); + } + }); + $command = new RunCommand( $state, $installationManager, $upgradeManager, $hookExecutor, - new EventDispatcher() + new EventDispatcher(), + $trackingService ); $tester = new CommandTester($command); @@ -115,6 +154,10 @@ public function testRunWithoutFullyInstalled(): void ->expects($this->once()) ->method('getPreviousVersion') ->willReturn('unknown'); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('10.6.0'); $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor @@ -131,12 +174,27 @@ public function testRunWithoutFullyInstalled(): void return true; })); + $trackingService = $this->createMock(TrackingService::class); + $trackingService + ->expects($this->exactly(2)) + ->method('track') + ->willReturnCallback(function ($event, $data): void { + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('10.6.0', $data['mysql_version']); + } + }); + $command = new RunCommand( $state, $installationManager, $this->createMock(UpgradeManager::class), $hookExecutor, - new EventDispatcher() + new EventDispatcher(), + $trackingService ); $tester = new CommandTester($command); diff --git a/tests/Services/InstallationManagerTest.php b/tests/Services/InstallationManagerTest.php index b78aefa..3875d60 100644 --- a/tests/Services/InstallationManagerTest.php +++ b/tests/Services/InstallationManagerTest.php @@ -15,6 +15,7 @@ use Shopware\Deployment\Services\InstallationManager; use Shopware\Deployment\Services\PluginHelper; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Output\OutputInterface; use Zalas\PHPUnit\Globals\Attribute\Env; @@ -39,6 +40,7 @@ public function testRun(): void $hookExecutor, new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -65,6 +67,7 @@ public function testRunNoStorefront(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -97,6 +100,7 @@ public function testRunDisabledAssetCopyAndThemeCompile(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(true, true), $this->createMock(OutputInterface::class)); @@ -127,6 +131,7 @@ public function testRunWithLicenseDomain(): void $hookExecutor, $configuration, $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -155,6 +160,7 @@ public function testRunWithForceReinstall(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(true, true, forceReinstallation: true), $this->createMock(OutputInterface::class)); diff --git a/tests/Services/ShopwareStateTest.php b/tests/Services/ShopwareStateTest.php index 5e21cfb..af9754b 100644 --- a/tests/Services/ShopwareStateTest.php +++ b/tests/Services/ShopwareStateTest.php @@ -7,6 +7,7 @@ use Composer\InstalledVersions; use Doctrine\DBAL\Connection; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Shopware\Deployment\Services\ShopwareState; @@ -225,4 +226,53 @@ public function testDisableMaintenanceMode(): void $this->state->disableMaintenanceMode(); } + + #[DataProvider('mysqlVersionProvider')] + public function testGetMySqlVersion(string $versionString, string $expectedResult): void + { + $this->connection + ->expects($this->once()) + ->method('fetchOne') + ->with('SELECT VERSION()') + ->willReturn($versionString); + + static::assertSame($expectedResult, $this->state->getMySqlVersion()); + } + + /** + * @return array + */ + public static function mysqlVersionProvider(): array + { + return [ + 'MySQL 8.0' => [ + 'versionString' => '8.0.23', + 'expectedResult' => 'mysql-8.0.23', + ], + 'MySQL 5.7 with Ubuntu suffix' => [ + 'versionString' => '5.7.33-0ubuntu0.18.04.1', + 'expectedResult' => 'mysql-5.7.33', + ], + 'MariaDB 10.5' => [ + 'versionString' => '10.5.9-MariaDB', + 'expectedResult' => 'mariadb-10.5', + ], + 'MariaDB with complex version string' => [ + 'versionString' => '5.5.5-10.6.7-MariaDB-1:10.6.7+maria~focal', + 'expectedResult' => 'mariadb-10.6', + ], + 'MariaDB alternative format' => [ + 'versionString' => 'mariadb-10.11.2', + 'expectedResult' => 'mariadb-10.11', + ], + 'MariaDB uppercase' => [ + 'versionString' => '10.3.31-MARIADB-0ubuntu0.20.04.1', + 'expectedResult' => 'mariadb-10.3', + ], + 'Percona Server' => [ + 'versionString' => '8.0.25-15', + 'expectedResult' => 'mysql-8.0.25', + ], + ]; + } } diff --git a/tests/Services/UpgradeManagerTest.php b/tests/Services/UpgradeManagerTest.php index 244a3fa..b77a5a3 100644 --- a/tests/Services/UpgradeManagerTest.php +++ b/tests/Services/UpgradeManagerTest.php @@ -14,6 +14,7 @@ use Shopware\Deployment\Services\OneTimeTasks; use Shopware\Deployment\Services\PluginHelper; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Services\UpgradeManager; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Output\OutputInterface; @@ -47,6 +48,7 @@ public function testRun(): void $oneTimeTasks, new ProjectConfiguration(), $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -56,12 +58,12 @@ public function testRunUpdatesVersion(): void { $state = $this->createMock(ShopwareState::class); $state - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getCurrentVersion') ->willReturn('1.0.0'); $state - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPreviousVersion') ->willReturn('0.0.0'); @@ -79,6 +81,7 @@ public function testRunUpdatesVersion(): void $this->createMock(OneTimeTasks::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -88,12 +91,12 @@ public function testRunUpdatesVersionNoAssetCompile(): void { $state = $this->createMock(ShopwareState::class); $state - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getCurrentVersion') ->willReturn('1.0.0'); $state - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPreviousVersion') ->willReturn('0.0.0'); @@ -120,6 +123,7 @@ public function testRunUpdatesVersionNoAssetCompile(): void $this->createMock(OneTimeTasks::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(true, true), $this->createMock(OutputInterface::class)); @@ -163,6 +167,7 @@ public function testRunWithDifferentSalesChannelUrl(): void $this->createMock(OneTimeTasks::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -205,6 +210,7 @@ public function testRunWithMaintenanceMode(): void $this->createMock(OneTimeTasks::class), $config, $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -242,6 +248,7 @@ public function testRunWithLicenseDomain(): void $oneTimeTasks, $configuration, $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); From e0bc008fc767f129f0fac88d27e998307dcba440 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 12:05:06 +0000 Subject: [PATCH 02/17] feat: add tests for one-time tasks table creation and MySQL version validation --- .devcontainer.json | 3 ++- .gitignore | 2 ++ tests/Services/ShopwareStateTest.php | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.devcontainer.json b/.devcontainer.json index 85f9e36..49677c8 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -19,7 +19,8 @@ } }, "containerEnv": { - "PROJECT_ROOT": "/var/www/html" + "PROJECT_ROOT": "/var/www/html", + "PHP_PROFILER": "xdebug" }, "mounts": [ "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", diff --git a/.gitignore b/.gitignore index 858b804..729ea06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /.idea +/.claude /vendor /composer.lock /var/cache /build/coverage +/coverage diff --git a/tests/Services/ShopwareStateTest.php b/tests/Services/ShopwareStateTest.php index af9754b..4f454c9 100644 --- a/tests/Services/ShopwareStateTest.php +++ b/tests/Services/ShopwareStateTest.php @@ -275,4 +275,18 @@ public static function mysqlVersionProvider(): array ], ]; } + + public function testGetMySqlVersionWithInvalidMariaDBVersion(): void + { + $this->connection + ->expects($this->once()) + ->method('fetchOne') + ->with('SELECT VERSION()') + ->willReturn('mariadb-invalid'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid version string: mariadb-invalid'); + + $this->state->getMySqlVersion(); + } } From c538c8e29bdaeefa41ac50f408e941ea9589b2eb Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 12:07:27 +0000 Subject: [PATCH 03/17] fix: initialize responses array in TrackingService --- src/Services/TrackingService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index fd758ed..5607e56 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -26,7 +26,7 @@ class TrackingService /** * @var list<\Symfony\Contracts\HttpClient\ResponseInterface> */ - private array $responses; + private array $responses = []; public function __construct( private readonly SystemConfigHelper $systemConfigHelper, From 2dbc751efcda66e725f481e94788433c6f3229c5 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 12:22:57 +0000 Subject: [PATCH 04/17] fix: add error handling for tracking MySQL version and deployment helper ID retrieval --- src/Command/RunCommand.php | 9 ++++++--- src/Services/TrackingService.php | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index c10ea84..6008ed3 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -47,9 +47,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, ]); - $this->trackingService->track('mysql_version', [ - 'mysql_version' => $this->state->getMySqlVersion(), - ]); + try { + $this->trackingService->track('mysql_version', [ + 'mysql_version' => $this->state->getMySqlVersion(), + ]); + } catch (\Throwable) { + } $timeout = $input->getOption('timeout'); diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index 5607e56..37e2a8f 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -90,7 +90,11 @@ private function getId(): string return $this->id; } - $id = $this->systemConfigHelper->get(self::DEPLOYMENT_HELPER_ID); + try { + $id = $this->systemConfigHelper->get(self::DEPLOYMENT_HELPER_ID); + } catch (\Throwable) { + $this->id = $id = bin2hex(random_bytes(16)); + } if ($id === null) { $id = bin2hex(random_bytes(16)); From 2efedc7896f203831ccd3fea16c37ed3e195a46d Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 12:47:25 +0000 Subject: [PATCH 05/17] fix: update RunCommand to track MySQL version conditionally and adjust tests accordingly --- src/Command/RunCommand.php | 7 +++---- src/Services/TrackingService.php | 10 ++++++++++ tests/Command/RunCommandTest.php | 15 +++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 6008ed3..8f9d139 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -47,11 +47,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, ]); - try { + $installed = $this->state->isInstalled(); + + if ($installed) { $this->trackingService->track('mysql_version', [ 'mysql_version' => $this->state->getMySqlVersion(), ]); - } catch (\Throwable) { } $timeout = $input->getOption('timeout'); @@ -63,8 +64,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int forceReinstallation: EnvironmentHelper::getVariable('SHOPWARE_DEPLOYMENT_FORCE_REINSTALL', '0') === '1', ); - $installed = $this->state->isInstalled(); - if ($config->forceReinstallation && $this->state->getPreviousVersion() === 'unknown') { $installed = false; } diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index 37e2a8f..478c96a 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -21,6 +21,8 @@ class TrackingService private string $id; + private bool $idNeedsToBePersisted = false; + private HttpClientInterface $client; /** @@ -94,6 +96,7 @@ private function getId(): string $id = $this->systemConfigHelper->get(self::DEPLOYMENT_HELPER_ID); } catch (\Throwable) { $this->id = $id = bin2hex(random_bytes(16)); + $this->idNeedsToBePersisted = true; } if ($id === null) { @@ -106,6 +109,13 @@ private function getId(): string private function shutdown(): void { + if ($this->idNeedsToBePersisted) { + try { + $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $this->id); + } catch (\Throwable) { + } + } + usleep(100); foreach ($this->responses as $response) { $response->cancel(); diff --git a/tests/Command/RunCommandTest.php b/tests/Command/RunCommandTest.php index 7865df3..84a85ec 100644 --- a/tests/Command/RunCommandTest.php +++ b/tests/Command/RunCommandTest.php @@ -26,10 +26,6 @@ public function testInstall(): void ->expects($this->once()) ->method('isInstalled') ->willReturn(false); - $state - ->expects($this->once()) - ->method('getMySqlVersion') - ->willReturn('8.0.0'); $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor @@ -50,16 +46,11 @@ public function testInstall(): void $trackingService = $this->createMock(TrackingService::class); $trackingService - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('track') ->willReturnCallback(function ($event, $data): void { - if ($event === 'php_version') { - static::assertArrayHasKey('php_version', $data); - static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); - } elseif ($event === 'mysql_version') { - static::assertArrayHasKey('mysql_version', $data); - static::assertEquals('8.0.0', $data['mysql_version']); - } + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); }); $command = new RunCommand( From 50f0692133ea90a52e3719e4f419ca8c84b7455a Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 12:48:54 +0000 Subject: [PATCH 06/17] fix: add CLAUDE_CONFIG_DIR to container environment in devcontainer configuration --- .devcontainer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer.json b/.devcontainer.json index 49677c8..60bad1e 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -20,7 +20,8 @@ }, "containerEnv": { "PROJECT_ROOT": "/var/www/html", - "PHP_PROFILER": "xdebug" + "PHP_PROFILER": "xdebug", + "CLAUDE_CONFIG_DIR": "/home/www-data/.claude/claude/" }, "mounts": [ "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", From dc0d8f07063c7080716cca3ab2cb09fd3c045859 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 13:17:00 +0000 Subject: [PATCH 07/17] fix: add CLAUDE_CONFIG_DIR and RW_DIRECTORIES to container environment; implement persistId method in TrackingService and update InstallationManagerTest to verify tracking --- .devcontainer.json | 3 ++- src/Services/InstallationManager.php | 1 + src/Services/TrackingService.php | 15 +++++---------- tests/Services/InstallationManagerTest.php | 5 ++++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.devcontainer.json b/.devcontainer.json index 60bad1e..362612e 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -21,7 +21,8 @@ "containerEnv": { "PROJECT_ROOT": "/var/www/html", "PHP_PROFILER": "xdebug", - "CLAUDE_CONFIG_DIR": "/home/www-data/.claude/claude/" + "CLAUDE_CONFIG_DIR": "/home/www-data/.claude/claude/", + "RW_DIRECTORIES": "/home/www-data/.claude" }, "mounts": [ "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", diff --git a/src/Services/InstallationManager.php b/src/Services/InstallationManager.php index a880083..d7c3032 100644 --- a/src/Services/InstallationManager.php +++ b/src/Services/InstallationManager.php @@ -60,6 +60,7 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $took = microtime(true); $this->processHelper->console(['system:install', '--create-database', '--shop-locale=' . $shopLocale, '--shop-currency=' . $shopCurrency, '--force', ...$additionalInstallParameters]); + $this->trackingService->persistId(); $this->trackingService->track('installed', ['took' => microtime(true) - $took]); $this->processHelper->console(['user:create', $adminUser, '--password=' . $adminPassword]); diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index 478c96a..be461c7 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -21,8 +21,6 @@ class TrackingService private string $id; - private bool $idNeedsToBePersisted = false; - private HttpClientInterface $client; /** @@ -70,6 +68,11 @@ public function track(string $eventName, array $tags = []): void ]); } + public function persistId(): void + { + $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $this->id); + } + /** * @return array */ @@ -96,7 +99,6 @@ private function getId(): string $id = $this->systemConfigHelper->get(self::DEPLOYMENT_HELPER_ID); } catch (\Throwable) { $this->id = $id = bin2hex(random_bytes(16)); - $this->idNeedsToBePersisted = true; } if ($id === null) { @@ -109,13 +111,6 @@ private function getId(): string private function shutdown(): void { - if ($this->idNeedsToBePersisted) { - try { - $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $this->id); - } catch (\Throwable) { - } - } - usleep(100); foreach ($this->responses as $response) { $response->cancel(); diff --git a/tests/Services/InstallationManagerTest.php b/tests/Services/InstallationManagerTest.php index 3875d60..3d38ae8 100644 --- a/tests/Services/InstallationManagerTest.php +++ b/tests/Services/InstallationManagerTest.php @@ -151,6 +151,9 @@ public function testRunWithForceReinstall(): void $accountService = $this->createMock(AccountService::class); $accountService->expects(static::never())->method('refresh'); + $trackingService = $this->createMock(TrackingService::class); + $trackingService->expects(static::once())->method('persistId'); + $manager = new InstallationManager( $this->createMock(ShopwareState::class), $this->createMock(Connection::class), @@ -160,7 +163,7 @@ public function testRunWithForceReinstall(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $accountService, - $this->createMock(TrackingService::class), + $trackingService, ); $manager->run(new RunConfiguration(true, true, forceReinstallation: true), $this->createMock(OutputInterface::class)); From d88094e389c6d8883d0c946cec4522c19c61768a Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 13:33:11 +0000 Subject: [PATCH 08/17] fix: refactor tracking in RunCommand to ensure PHP and MySQL versions are tracked consistently; update tests to verify tracking behavior --- src/Command/RunCommand.php | 19 ++++++++----------- tests/Command/RunCommandTest.php | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 8f9d139..1a0c081 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -43,18 +43,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $this->trackingService->track('php_version', [ - 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, - ]); - $installed = $this->state->isInstalled(); - - if ($installed) { - $this->trackingService->track('mysql_version', [ - 'mysql_version' => $this->state->getMySqlVersion(), - ]); - } - $timeout = $input->getOption('timeout'); $config = new RunConfiguration( @@ -76,6 +65,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->installationManager->run($config, $output); } + $this->trackingService->track('php_version', [ + 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, + ]); + + $this->trackingService->track('mysql_version', [ + 'mysql_version' => $this->state->getMySqlVersion(), + ]); + $this->eventDispatcher->dispatch(new PostDeploy($config, $output)); $this->hookExecutor->execute(HookExecutor::HOOK_POST); diff --git a/tests/Command/RunCommandTest.php b/tests/Command/RunCommandTest.php index 84a85ec..e732185 100644 --- a/tests/Command/RunCommandTest.php +++ b/tests/Command/RunCommandTest.php @@ -27,6 +27,11 @@ public function testInstall(): void ->method('isInstalled') ->willReturn(false); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('5.7.0'); + $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor ->expects($this->exactly(2)) @@ -46,11 +51,16 @@ public function testInstall(): void $trackingService = $this->createMock(TrackingService::class); $trackingService - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('track') ->willReturnCallback(function ($event, $data): void { - static::assertArrayHasKey('php_version', $data); - static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('5.7.0', $data['mysql_version']); + } }); $command = new RunCommand( From ff29402654690707da34570d0b5882d43cfb22eb Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 13:35:19 +0000 Subject: [PATCH 09/17] fix: remove unused variable comment in track method to improve code clarity --- src/Services/TrackingService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index be461c7..3136274 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -51,7 +51,6 @@ public function track(string $eventName, array $tags = []): void $tags += $this->getTags(); $id = $this->getId(); - // The variable is unused on purpose, otherwise __destruct on Response object is directly called, which makes this request synchronously $this->responses[] = $this->client->request('PUT', '/track', [ 'json' => [ 'event' => 'deployment_helper.' . $eventName, From a1842819badb058f8fbeae1c2a700eefde58fc9e Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 13:38:02 +0000 Subject: [PATCH 10/17] fix: ensure id is set correctly in getId method of TrackingService --- src/Services/TrackingService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index 3136274..0292c59 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -101,7 +101,7 @@ private function getId(): string } if ($id === null) { - $id = bin2hex(random_bytes(16)); + $this->id = $id = bin2hex(random_bytes(16)); $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $id); } From a9c284f2b50d0635b0035cad3d3324bd126cd188 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 13:56:04 +0000 Subject: [PATCH 11/17] fix: add check for id existence before setting it in persistId method of TrackingService --- src/Services/TrackingService.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index 0292c59..5ebb3e9 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -69,7 +69,9 @@ public function track(string $eventName, array $tags = []): void public function persistId(): void { - $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $this->id); + if (isset($this->id)) { + $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $this->id); + } } /** From 383f45ec1c840f2229d37dbd92d2d953a04dbcbd Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 20 Aug 2025 14:16:54 +0000 Subject: [PATCH 12/17] fix: update tracking endpoint in track method of TrackingService to use correct URL --- composer.json | 1 + src/Command/RunCommand.php | 26 +++++++------- src/Services/InstallationManager.php | 2 +- src/Services/TrackingService.php | 54 ++++++++++------------------ 4 files changed, 34 insertions(+), 49 deletions(-) diff --git a/composer.json b/composer.json index 644a373..31db51b 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "php": ">=8.2", "ext-dom": "*", "ext-pdo": "*", + "ext-sockets": "*", "digilist/dependency-graph": ">=0.4.1", "doctrine/dbal": "^3.0 || ^4.0", "symfony/config": "^7.0 || ^6.0", diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 1a0c081..78fd460 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -59,19 +59,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->hookExecutor->execute(HookExecutor::HOOK_PRE); - if ($installed) { - $this->upgradeManager->run($config, $output); - } else { - $this->installationManager->run($config, $output); - } - - $this->trackingService->track('php_version', [ - 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, - ]); + try { + if ($installed) { + $this->upgradeManager->run($config, $output); + } else { + $this->installationManager->run($config, $output); + } + } finally { + $this->trackingService->track('php_version', [ + 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, + ]); - $this->trackingService->track('mysql_version', [ - 'mysql_version' => $this->state->getMySqlVersion(), - ]); + $this->trackingService->track('mysql_version', [ + 'mysql_version' => $this->state->getMySqlVersion(), + ]); + } $this->eventDispatcher->dispatch(new PostDeploy($config, $output)); diff --git a/src/Services/InstallationManager.php b/src/Services/InstallationManager.php index d7c3032..4aeeb1a 100644 --- a/src/Services/InstallationManager.php +++ b/src/Services/InstallationManager.php @@ -61,7 +61,7 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->processHelper->console(['system:install', '--create-database', '--shop-locale=' . $shopLocale, '--shop-currency=' . $shopCurrency, '--force', ...$additionalInstallParameters]); $this->trackingService->persistId(); - $this->trackingService->track('installed', ['took' => microtime(true) - $took]); + $this->trackingService->track('installed', ['took' => microtime(true) - $took, 'shopware_version' => $this->state->getCurrentVersion()]); $this->processHelper->console(['user:create', $adminUser, '--password=' . $adminPassword]); diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php index 5ebb3e9..016fce3 100644 --- a/src/Services/TrackingService.php +++ b/src/Services/TrackingService.php @@ -5,15 +5,13 @@ namespace Shopware\Deployment\Services; use Shopware\Deployment\Helper\EnvironmentHelper; -use Symfony\Component\HttpClient\HttpClient; -use Symfony\Contracts\HttpClient\HttpClientInterface; class TrackingService { - private const API_ENDPOINT = 'https://usage.shopware.io'; - private const DEPLOYMENT_HELPER_ID = 'core.deployment_helper.id'; + private const DEFAULT_TRACKING_DOMAIN = 'udp.usage.shopware.io'; + /** * @var array */ @@ -21,22 +19,16 @@ class TrackingService private string $id; - private HttpClientInterface $client; + private \Socket|false $socket; - /** - * @var list<\Symfony\Contracts\HttpClient\ResponseInterface> - */ - private array $responses = []; + private string $domain; public function __construct( private readonly SystemConfigHelper $systemConfigHelper, private readonly ShopwareState $shopwareState, ) { - $this->client = HttpClient::create([ - 'base_uri' => EnvironmentHelper::getVariable('SHOPWARE_TRACKING_ENDPOINT', self::API_ENDPOINT), - ]); - - register_shutdown_function([$this, 'shutdown']); + $this->socket = @socket_create(\AF_INET, \SOCK_DGRAM, \SOL_UDP); + $this->domain = EnvironmentHelper::getVariable('SHOPWARE_TRACKING_DOMAIN', self::DEFAULT_TRACKING_DOMAIN); } /** @@ -51,20 +43,18 @@ public function track(string $eventName, array $tags = []): void $tags += $this->getTags(); $id = $this->getId(); - $this->responses[] = $this->client->request('PUT', '/track', [ - 'json' => [ - 'event' => 'deployment_helper.' . $eventName, - 'tags' => $tags, - 'user_id' => $id, - 'timestamp' => (new \DateTime())->format(\DateTime::ISO8601), - ], - 'timeout' => 0.8, - 'max_duration' => 0.8, - 'headers' => [ - 'User-Agent' => 'shopware-deployment-helper', - 'Accept' => 'application/json', - ], - ]); + if ($this->socket === false) { + return; + } + + $payload = json_encode([ + 'event' => 'deployment_helper.' . $eventName, + 'tags' => $tags, + 'user_id' => $id, + 'timestamp' => (new \DateTime())->format(\DateTimeInterface::ATOM), + ], \JSON_THROW_ON_ERROR); + + @socket_sendto($this->socket, $payload, \strlen($payload), 0, $this->domain, 9000); } public function persistId(): void @@ -109,12 +99,4 @@ private function getId(): string return $this->id = $id; } - - private function shutdown(): void - { - usleep(100); - foreach ($this->responses as $response) { - $response->cancel(); - } - } } From 7d661699cbfa25d76c38edf545a2aae88b2fd3f4 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 24 Dec 2025 07:40:57 +0100 Subject: [PATCH 13/17] feat: add ConsoleOutputInterface and DefaultConsoleOutput for improved output handling; refactor ProcessHelper to utilize new output methods --- .gitignore | 1 + src/Helper/ConsoleOutputInterface.php | 12 ++++ src/Helper/DefaultConsoleOutput.php | 18 ++++++ src/Helper/ProcessHelper.php | 70 +++++++++++------------- tests/Helper/ProcessHelperTest.php | 15 ++++- tests/Services/TrackingServiceTest.php | 66 ++++++++++++++++++++++ tests/TestUtil/BufferedConsoleOutput.php | 33 +++++++++++ 7 files changed, 175 insertions(+), 40 deletions(-) create mode 100644 src/Helper/ConsoleOutputInterface.php create mode 100644 src/Helper/DefaultConsoleOutput.php create mode 100644 tests/Services/TrackingServiceTest.php create mode 100644 tests/TestUtil/BufferedConsoleOutput.php diff --git a/.gitignore b/.gitignore index 729ea06..201a14f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /var/cache /build/coverage /coverage +/.junie diff --git a/src/Helper/ConsoleOutputInterface.php b/src/Helper/ConsoleOutputInterface.php new file mode 100644 index 0000000..141d5af --- /dev/null +++ b/src/Helper/ConsoleOutputInterface.php @@ -0,0 +1,12 @@ +timeout = $this->validateTimeout($timeout ?? (float) EnvironmentHelper::getVariable('SHOPWARE_DEPLOYMENT_TIMEOUT', '60')); } @@ -32,17 +33,11 @@ public function shell(array $command): void $startTime = $this->printPreStart($command); - if (\function_exists('stream_isatty') && stream_isatty(\STDOUT)) { + if ($this->output instanceof DefaultConsoleOutput && \function_exists('stream_isatty') && stream_isatty(\STDOUT)) { $process->setTty(true); } - $process->run(function (string $type, string $buffer): void { - if ($type === Process::ERR) { - fwrite(\STDERR, $buffer); - } else { - fwrite(\STDOUT, $buffer); - } - }); + $process->run($this->output(...)); if (!$process->isSuccessful()) { throw new \RuntimeException('Execution of ' . implode(' ', $command) . ' failed'); @@ -63,17 +58,11 @@ public function run(array $args): void $startTime = $this->printPreStart($completeCmd); - if (\function_exists('stream_isatty') && stream_isatty(\STDOUT)) { + if ($this->output instanceof DefaultConsoleOutput && \function_exists('stream_isatty') && stream_isatty(\STDOUT)) { $process->setTty(true); } - $process->run(function (string $type, string $buffer): void { - if ($type === Process::ERR) { - fwrite(\STDERR, $buffer); - } else { - fwrite(\STDOUT, $buffer); - } - }); + $process->run($this->output(...)); if (!$process->isSuccessful()) { throw new \RuntimeException('Execution of ' . implode(' ', $args) . ' failed'); @@ -100,17 +89,11 @@ public function runAndTail(string $code): void $process = new Process(['sh', '-c', $code], $this->projectDir); $process->setTimeout($this->timeout); - if (\function_exists('stream_isatty') && stream_isatty(\STDOUT)) { + if ($this->output instanceof DefaultConsoleOutput && \function_exists('stream_isatty') && stream_isatty(\STDOUT)) { $process->setTty(true); } - $process->run(function (string $type, string $buffer): void { - if ($type === Process::ERR) { - fwrite(\STDERR, $buffer); - } else { - fwrite(\STDOUT, $buffer); - } - }); + $process->run($this->output(...)); if (!$process->isSuccessful()) { throw new \RuntimeException('Execution of ' . $originalCode . ' failed'); @@ -140,6 +123,15 @@ public function setTimeout(?float $timeout): static return $this; } + private function output(string $type, string $buffer): void + { + if ($type === Process::ERR) { + $this->output->writeStderr($buffer); + } else { + $this->output->writeStdout($buffer); + } + } + /** * @param array $cmd */ @@ -148,14 +140,14 @@ private function printPreStart(array $cmd): float $cmdString = implode(' ', $cmd); $startTime = microtime(true); - fwrite(\STDOUT, \PHP_EOL); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, "============== [deployment-helper] ==============\n"); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \sprintf("Start: %s\n", $cmdString)); - fwrite(\STDOUT, \sprintf("Time limit: %s seconds\n", $this->timeout)); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \PHP_EOL); + $this->output->writeStdout(\PHP_EOL); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout("============== [deployment-helper] ==============\n"); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\sprintf("Start: %s\n", $cmdString)); + $this->output->writeStdout(\sprintf("Time limit: %s seconds\n", $this->timeout)); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\PHP_EOL); return $startTime; } @@ -165,17 +157,17 @@ private function printPreStart(array $cmd): float */ private function printPostStart(array $cmd, float $startTime): void { - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, "============== [deployment-helper] ==============\n"); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \sprintf("End: %s\n", implode(' ', $cmd))); - fwrite(\STDOUT, \sprintf( + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout("============== [deployment-helper] ==============\n"); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\sprintf("End: %s\n", implode(' ', $cmd))); + $this->output->writeStdout(\sprintf( "> Time: %sms\n", number_format((microtime(true) - $startTime) * 1000, 2, '.', ''), )); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \PHP_EOL); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\PHP_EOL); } /** diff --git a/tests/Helper/ProcessHelperTest.php b/tests/Helper/ProcessHelperTest.php index 0e923e0..96ccf01 100644 --- a/tests/Helper/ProcessHelperTest.php +++ b/tests/Helper/ProcessHelperTest.php @@ -7,16 +7,29 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Shopware\Deployment\Helper\ProcessHelper; +use Shopware\Deployment\Tests\TestUtil\BufferedConsoleOutput; #[CoversClass(ProcessHelper::class)] class ProcessHelperTest extends TestCase { + public function testOutputIsBuffered(): void + { + $output = new BufferedConsoleOutput(); + $helper = new ProcessHelper('/tmp', output: $output); + + $helper->runAndTail('echo "test"'); + + static::assertStringContainsString('Start: echo "test"', $output->getStdout()); + static::assertStringContainsString('test', $output->getStdout()); + static::assertStringContainsString('End: echo "test"', $output->getStdout()); + } + public function testRunAndTailReplacesPhpBinPlaceholder(): void { $tempFile = tempnam(sys_get_temp_dir(), 'php_bin_test_'); try { - $helper = new ProcessHelper('/tmp'); + $helper = new ProcessHelper('/tmp', output: new BufferedConsoleOutput()); $helper->runAndTail('echo %php.bin% > ' . $tempFile); static::assertFileExists($tempFile); diff --git a/tests/Services/TrackingServiceTest.php b/tests/Services/TrackingServiceTest.php new file mode 100644 index 0000000..ed8516b --- /dev/null +++ b/tests/Services/TrackingServiceTest.php @@ -0,0 +1,66 @@ +createMock(ShopwareState::class); + $shopwareState->method('getCurrentVersion')->willReturn('6.6.0.0'); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->track('test_event'); + + $id = $systemConfigHelper->get('core.deployment_helper.id'); + static::assertNotNull($id); + static::assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $id); + } + + public function testTrackUsesExistingIdFromConfig(): void + { + $systemConfigHelper = new StaticSystemConfigHelper(); + $systemConfigHelper->set('core.deployment_helper.id', 'existing-id-123'); + $shopwareState = $this->createMock(ShopwareState::class); + $shopwareState->method('getCurrentVersion')->willReturn('6.6.0.0'); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->track('test_event'); + + static::assertSame('existing-id-123', $systemConfigHelper->get('core.deployment_helper.id')); + } + + public function testPersistIdDoesNothingWhenNoIdGenerated(): void + { + $systemConfigHelper = new StaticSystemConfigHelper(); + $shopwareState = $this->createMock(ShopwareState::class); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->persistId(); + + static::assertNull($systemConfigHelper->get('core.deployment_helper.id')); + } + + #[Env('DO_NOT_TRACK')] + #[DoesNotPerformAssertions] + public function testTrackWithDoNotTrackEnv(): void + { + $systemConfigHelper = new StaticSystemConfigHelper(); + $shopwareState = $this->createMock(ShopwareState::class); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->track('test_event'); + } +} diff --git a/tests/TestUtil/BufferedConsoleOutput.php b/tests/TestUtil/BufferedConsoleOutput.php new file mode 100644 index 0000000..afb9490 --- /dev/null +++ b/tests/TestUtil/BufferedConsoleOutput.php @@ -0,0 +1,33 @@ +stdout .= $message; + } + + public function writeStderr(string $message): void + { + $this->stderr .= $message; + } + + public function getStdout(): string + { + return $this->stdout; + } + + public function getStderr(): string + { + return $this->stderr; + } +} From 01df54c052122b199916772f2d74fe93b42987aa Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 24 Dec 2025 08:01:53 +0100 Subject: [PATCH 14/17] feat: disable telemetry during test runs by adding DO_NOT_TRACK variable in integration.yml --- .github/workflows/integration.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 5f978ed..8ebb325 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -11,6 +11,8 @@ env: APP_ENV: prod SALES_CHANNEL_URL: http://localhost:8000 DATABASE_URL: mysql://root@127.0.0.1/shopware + # Disable our own telemetry to not fill test runs + DO_NOT_TRACK: 1 jobs: installation: From 30cbd25bbb3863833665d9c0d45d63cd75e106b2 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 24 Dec 2025 08:05:01 +0100 Subject: [PATCH 15/17] feat: update paths in integration.yml to use static-plugins directory for deployment helper --- .github/workflows/integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 8ebb325..9476937 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -39,10 +39,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: ./custom/plugins/deployment-helper + path: ./custom/static-plugins/deployment-helper - name: Set fake version into deployment helper - run: composer -d custom/plugins/deployment-helper config version 999.9.9 + run: composer -d custom/static-plugins/deployment-helper config version 999.9.9 - name: Install Deployment Helper run: composer require --dev 'shopware/deployment-helper:*' @@ -88,10 +88,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: ./custom/plugins/deployment-helper + path: ./custom/static-plugins/deployment-helper - name: Set fake version into deployment helper - run: composer -d custom/plugins/deployment-helper config version 999.9.9 + run: composer -d custom/static-plugins/deployment-helper config version 999.9.9 - name: Install Deployment Helper run: composer require --dev 'shopware/deployment-helper:*' From d36337e1e0686109d783d00de16e895aa5a437ca Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 24 Dec 2025 08:09:38 +0100 Subject: [PATCH 16/17] feat: update integration.yml and shopware-deployment-helper to support static-plugins directory --- .github/workflows/integration.yml | 4 ++-- bin/shopware-deployment-helper | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 9476937..3160eaa 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -148,10 +148,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: ./custom/plugins/deployment-helper + path: ./custom/static-plugins/deployment-helper - name: Set fake version into deployment helper - run: composer -d custom/plugins/deployment-helper config version 999.9.9 + run: composer -d custom/static-plugins/deployment-helper config version 999.9.9 - name: Install Deployment Helper run: composer require --dev 'shopware/deployment-helper:*' diff --git a/bin/shopware-deployment-helper b/bin/shopware-deployment-helper index 6b49ca0..696bc69 100755 --- a/bin/shopware-deployment-helper +++ b/bin/shopware-deployment-helper @@ -17,7 +17,7 @@ $includables = [ __DIR__ . '/vendor/autoload.php', ]; -if (str_contains(__DIR__, 'custom/plugins')) { +if (str_contains(__DIR__, 'custom/plugins') || str_contains(__DIR__, 'custom/static-plugins')) { $includables[] = __DIR__ . '/../../../../vendor/autoload.php'; } From 94a06f0e9e52e673f7e6965d98112108629fc5f5 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 24 Dec 2025 08:10:55 +0100 Subject: [PATCH 17/17] fix: remove devcontainer.json --- .devcontainer.json | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 .devcontainer.json diff --git a/.devcontainer.json b/.devcontainer.json deleted file mode 100644 index 362612e..0000000 --- a/.devcontainer.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "image": "ghcr.io/shopwarelabs/devcontainer/symfony-flex:6.6.10-8.3", - "overrideCommand": false, - "updateRemoteUserUID": false, - "forwardPorts": [ - 8000 - ], - "features": { - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {} - }, - "portsAttributes": { - "8000": { - "label": "Shopware", - "onAutoForward": "notify" - }, - "8080": { - "label": "Administration Watcher", - "onAutoForward": "notify" - } - }, - "containerEnv": { - "PROJECT_ROOT": "/var/www/html", - "PHP_PROFILER": "xdebug", - "CLAUDE_CONFIG_DIR": "/home/www-data/.claude/claude/", - "RW_DIRECTORIES": "/home/www-data/.claude" - }, - "mounts": [ - "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", - "source=claude-code-config-${devcontainerId},target=/home/www-data/.claude,type=volume" - ], - "customizations": { - "vscode": { - "extensions": [ - "DEVSENSE.phptools-vscode", - "redhat.vscode-yaml" - ] - } - } -} \ No newline at end of file