diff --git a/README.md b/README.md index c0d1ba9..aba2782 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Running the tools can be done by calling their respective binary: ### PHP CS Fixer Initialize the configuration with: -```bash +```bash $ php vendor/bin/prestashop-coding-standards cs-fixer:init [--dest /path/to/my/project] ``` @@ -99,3 +99,17 @@ $ vendor/bin/header-stamp --license=assets/afl.txt --exclude=vendor,node_modules ``` Available options are provided with `--help`. + +### Bump Module Version + +Bump your module version number using [Semantic Versioning](https://semver.org/) in preparation for new release. + +Updates module `$this->version` value in the module main php class file and `` tag in `config.xml` file (if present). + +```bash +$ php vendor/bin/prestashop-coding-standards bump-version patch # 1.2.3 → 1.2.4 +$ php vendor/bin/prestashop-coding-standards bump-version minor # 1.2.3 → 1.3.0 +$ php vendor/bin/prestashop-coding-standards bump-version major # 1.2.3 → 2.0.0 +``` + +The current version is read from the module main php file `$this->version` and bumped diff --git a/bin/prestashop-coding-standards b/bin/prestashop-coding-standards index ff23d1c..9868320 100755 --- a/bin/prestashop-coding-standards +++ b/bin/prestashop-coding-standards @@ -15,8 +15,10 @@ if (file_exists($autoloadFile)) { use Symfony\Component\Console\Application; use PrestaShop\CodingStandards\Command\CsFixerInitCommand; use PrestaShop\CodingStandards\Command\PhpStanInitCommand; +use PrestaShop\CodingStandards\Command\BumpVersionCommand; $app = new Application(); $app->add(new CsFixerInitCommand()); $app->add(new PhpStanInitCommand()); +$app->add(new BumpVersionCommand()); $app->run(); diff --git a/composer.json b/composer.json index 6d49bda..125eaa0 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "require": { "php": ">=7.2.5", "symfony/console": "~3.2 || ~4.0 || ~5.0 || ~6.0 || ~7.0", - "symfony/filesystem": "~3.2 || ~4.0 || ~5.0 || ~6.0 || ~7.0" + "symfony/filesystem": "~3.2 || ~4.0 || ~5.0 || ~6.0 || ~7.0", + "composer/semver": "^3.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2" diff --git a/composer.lock b/composer.lock index 649bfbf..c92c749 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,85 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "027badef563e13f110734a95ed41fb10", + "content-hash": "08cb640f020e9d5d0e449ab951aa27a6", "packages": [ + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, { "name": "psr/container", "version": "1.1.1", @@ -1063,87 +1140,6 @@ ], "time": "2022-01-21T20:24:37+00:00" }, - { - "name": "composer/semver", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2023-08-31T09:50:34+00:00" - }, { "name": "composer/xdebug-handler", "version": "2.0.5", @@ -2139,13 +2135,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=7.2.5" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.2.5" }, diff --git a/src/Command/BumpVersionCommand.php b/src/Command/BumpVersionCommand.php new file mode 100644 index 0000000..26fd5ab --- /dev/null +++ b/src/Command/BumpVersionCommand.php @@ -0,0 +1,235 @@ +setName('bump-version') + ->setDescription('Bump the version number') + ->addArgument( + 'type', + InputArgument::REQUIRED, + 'Version bump type: ' . implode(', ', self::SEMVER_TYPES) + ) + ->addOption( + 'path', + 'p', + InputOption::VALUE_REQUIRED, + 'Path to the module directory', + '.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $type = $input->getArgument('type'); + $modulePath = $input->getOption('path'); + + if (!in_array($type, self::SEMVER_TYPES)) { + $output->writeln('Invalid version type. Valid values are: ' . implode(', ', self::SEMVER_TYPES) . ''); + return self::INVALID; + } + + // Check that we can find the main module file + $mainModuleFile = $this->findMainModuleFile($modulePath); + if (!$mainModuleFile) { + $output->writeln('Could not find main module file'); + return self::FAILURE; + } + + // Get current version from the module file + $currentVersion = $this->getCurrentVersion($mainModuleFile); + if (!$currentVersion) { + $output->writeln('Could not find current version in module file'); + return self::FAILURE; + } + + // Strip to core version (major.minor.patch) if needed + $originalVersion = $currentVersion; + if (preg_match('/^(\d+\.\d+\.\d+)/', $currentVersion, $matches)) { + $currentVersion = $matches[1]; + if ($currentVersion !== $originalVersion) { + $output->writeln(sprintf('Stripped version from %s to %s', $originalVersion, $currentVersion)); + } + } + + // Validate using composer/semver + $parser = new VersionParser(); + try { + $parser->normalize($currentVersion); + } catch (\UnexpectedValueException $e) { + $output->writeln(sprintf('Invalid semantic version in module: %s', $currentVersion)); + $output->writeln(sprintf('%s', $e->getMessage())); + return self::FAILURE; + } + + $output->writeln(sprintf('Current version: %s', $currentVersion)); + + // Bump the version + $newVersion = $this->bumpVersion($currentVersion, $type); + $output->writeln(sprintf('New version: %s', $newVersion)); + + // Update module PHP file + $this->updateModuleFile($mainModuleFile, $currentVersion, $newVersion); + $output->writeln(sprintf('Updated %s', basename($mainModuleFile))); + + // Update config.xml if it exists + $configFile = dirname($mainModuleFile) . '/config.xml'; + if (file_exists($configFile)) { + $configVersion = $this->getConfigVersion($configFile); + if ($configVersion && $configVersion !== $currentVersion) { + $output->writeln(sprintf('Warning: config.xml version (%s) differs from module version (%s)', $configVersion, $currentVersion)); + // Force update by using the old version + $this->updateConfigFile($configFile, $configVersion, $newVersion); + } else { + // Versions match, update normally + $this->updateConfigFile($configFile, $currentVersion, $newVersion); + } + $output->writeln('Updated config.xml'); + } else { + $output->writeln('config.xml not found, skipping'); + } + + $output->writeln(sprintf('Successfully bumped version from %s to %s', $currentVersion, $newVersion)); + + return self::SUCCESS; + } + + private function findMainModuleFile(string $path): ?string + { + $fs = new Filesystem(); + + // Look for PHP files in the directory that match the directory name + $dirName = basename(realpath($path)); + $possibleFile = $path . '/' . $dirName . '.php'; + + if ($fs->exists($possibleFile)) { + return $possibleFile; + } + + // If not found, look for any PHP file that extends Module class + $files = glob($path . '/*.php'); + foreach ($files as $file) { + try { + $content = file_get_contents($file); + if (preg_match('/class\s+\w+\s+extends\s+Module/i', $content)) { + return $file; + } + } catch (\Exception $e) { + // Skip files that can't be read + continue; + } + } + + return null; + } + + private function getCurrentVersion(string $filePath): ?string + { + try { + $content = file_get_contents($filePath); + } catch (\Exception $e) { + return null; + } + + // Extract whatever is in $this->version = '...' + if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $matches)) { + return trim($matches[1]); + } + + return null; + } + + private function getConfigVersion(string $filePath): ?string + { + try { + $content = file_get_contents($filePath); + } catch (\Exception $e) { + return null; + } + + // Extract version from ... or + if (preg_match('/(?:)?<\/version>/', $content, $matches)) { + return trim($matches[1]); + } + + return null; + } + + private function bumpVersion(string $version, string $type): string + { + // Parse version components (already validated by normalize()) + list($major, $minor, $patch) = explode('.', $version); + $major = (int)$major; + $minor = (int)$minor; + $patch = (int)$patch; + + // Apply semver bump + switch ($type) { + case 'major': + $major++; + $minor = 0; + $patch = 0; + break; + case 'minor': + $minor++; + $patch = 0; + break; + case 'patch': + $patch++; + break; + } + + return sprintf('%d.%d.%d', $major, $minor, $patch); + } + + private function updateModuleFile(string $filePath, string $oldVersion, string $newVersion): void + { + try { + $content = file_get_contents($filePath); + + // Update $this->version + $content = preg_replace( + '/(\$this->version\s*=\s*[\'"])' . preg_quote($oldVersion, '/') . '([\'"])/i', + '${1}' . $newVersion . '${2}', + $content + ); + + file_put_contents($filePath, $content); + } catch (\Exception $e) { + throw new \RuntimeException(sprintf('Failed to update module file: %s', $e->getMessage()), 0, $e); + } + } + + private function updateConfigFile(string $filePath, string $oldVersion, string $newVersion): void + { + try { + $content = file_get_contents($filePath); + + // Update x.x.x or + $content = preg_replace( + '/()?<\/version>/i', + '${1}' . $newVersion . '${2}', + $content + ); + + file_put_contents($filePath, $content); + } catch (\Exception $e) { + throw new \RuntimeException(sprintf('Failed to update config file: %s', $e->getMessage()), 0, $e); + } + } +} diff --git a/tests/modules_samples/constant_check/config.xml b/tests/modules_samples/constant_check/config.xml new file mode 100644 index 0000000..2a2296a --- /dev/null +++ b/tests/modules_samples/constant_check/config.xml @@ -0,0 +1,9 @@ + + + constant_check + + + + + + diff --git a/tests/test-bump-version.sh b/tests/test-bump-version.sh new file mode 100755 index 0000000..f69ddd1 --- /dev/null +++ b/tests/test-bump-version.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +# Test script for bump-version command +# This script tests the version bumping functionality using the constant_check module + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "==========================================" +echo "Testing bump-version command" +echo "==========================================" + +# Get absolute path to the binary +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BIN_PATH="${SCRIPT_DIR}/../bin/prestashop-coding-standards" + +# Create temporary test directory +TEST_DIR="$(mktemp -d)" +echo -e "${YELLOW}Creating test directory: ${TEST_DIR}${NC}" + +# Copy the constant_check module to temp directory +cp -r modules_samples/constant_check "${TEST_DIR}/" +cd "${TEST_DIR}/constant_check" + +# Function to get version from PHP file +get_php_version() { + grep -oP "\\\$this->version = '\K[^']+" constant_check.php +} + +# Function to get version from config.xml +get_xml_version() { + grep -oP ' 1.0.1)${NC}" +bump_version patch + +PHP_VERSION=$(get_php_version) +XML_VERSION=$(get_xml_version) +echo " PHP version: ${PHP_VERSION}" +echo " XML version: ${XML_VERSION}" + +if [ "${PHP_VERSION}" != "1.0.1" ] || [ "${XML_VERSION}" != "1.0.1" ]; then + echo -e "${RED}FAIL: Expected 1.0.1, got PHP: ${PHP_VERSION}, XML: ${XML_VERSION}${NC}" + exit 1 +fi +echo -e "${GREEN}PASS${NC}" + +# Test 2: Bump patch again +echo "" +echo -e "${YELLOW}Test 2: Bumping patch version again (1.0.1 -> 1.0.2)${NC}" +bump_version patch + +PHP_VERSION=$(get_php_version) +XML_VERSION=$(get_xml_version) +echo " PHP version: ${PHP_VERSION}" +echo " XML version: ${XML_VERSION}" + +if [ "${PHP_VERSION}" != "1.0.2" ] || [ "${XML_VERSION}" != "1.0.2" ]; then + echo -e "${RED}FAIL: Expected 1.0.2, got PHP: ${PHP_VERSION}, XML: ${XML_VERSION}${NC}" + exit 1 +fi +echo -e "${GREEN}PASS${NC}" + +# Test 3: Bump minor version +echo "" +echo -e "${YELLOW}Test 3: Bumping minor version (1.0.2 -> 1.1.0)${NC}" +bump_version minor + +PHP_VERSION=$(get_php_version) +XML_VERSION=$(get_xml_version) +echo " PHP version: ${PHP_VERSION}" +echo " XML version: ${XML_VERSION}" + +if [ "${PHP_VERSION}" != "1.1.0" ] || [ "${XML_VERSION}" != "1.1.0" ]; then + echo -e "${RED}FAIL: Expected 1.1.0, got PHP: ${PHP_VERSION}, XML: ${XML_VERSION}${NC}" + exit 1 +fi +echo -e "${GREEN}PASS${NC}" + +# Test 4: Bump major version +echo "" +echo -e "${YELLOW}Test 4: Bumping major version (1.1.0 -> 2.0.0)${NC}" +bump_version major + +PHP_VERSION=$(get_php_version) +XML_VERSION=$(get_xml_version) +echo " PHP version: ${PHP_VERSION}" +echo " XML version: ${XML_VERSION}" + +if [ "${PHP_VERSION}" != "2.0.0" ] || [ "${XML_VERSION}" != "2.0.0" ]; then + echo -e "${RED}FAIL: Expected 2.0.0, got PHP: ${PHP_VERSION}, XML: ${XML_VERSION}${NC}" + exit 1 +fi +echo -e "${GREEN}PASS${NC}" + +# Test 5: Multiple minor bumps +echo "" +echo -e "${YELLOW}Test 5: Multiple minor bumps (2.0.0 -> 2.1.0 -> 2.2.0)${NC}" +bump_version minor +PHP_VERSION=$(get_php_version) +if [ "${PHP_VERSION}" != "2.1.0" ]; then + echo -e "${RED}FAIL: Expected 2.1.0, got ${PHP_VERSION}${NC}" + exit 1 +fi + +bump_version minor +PHP_VERSION=$(get_php_version) +XML_VERSION=$(get_xml_version) +echo " PHP version: ${PHP_VERSION}" +echo " XML version: ${XML_VERSION}" + +if [ "${PHP_VERSION}" != "2.2.0" ] || [ "${XML_VERSION}" != "2.2.0" ]; then + echo -e "${RED}FAIL: Expected 2.2.0, got PHP: ${PHP_VERSION}, XML: ${XML_VERSION}${NC}" + exit 1 +fi +echo -e "${GREEN}PASS${NC}" + +# Cleanup +echo "" +echo -e "${YELLOW}Cleaning up test directory${NC}" +rm -rf "${TEST_DIR}" + +echo "" +echo "==========================================" +echo -e "${GREEN}All tests passed successfully!${NC}" +echo "=========================================="