diff --git a/.gitattributes b/.gitattributes index 06f47c0..b67aae4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,9 +8,10 @@ /CODE_OF_CONDUCT.md export-ignore /deptrac.yaml export-ignore /ecs.php export-ignore -/infection.json.dist export-ignore +/infection.json export-ignore /Makefile export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore /rector.php export-ignore +/castor.php export-ignore diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index edf763c..4747edd 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow -name: "Integrate" +name: "Infection" on: push: @@ -18,6 +18,7 @@ jobs: php-version: "8.3" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "xdebug" + tools: castor - name: "Checkout code" uses: "actions/checkout@v3" @@ -32,4 +33,4 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Infection" - run: "make ci-mu" + run: "castor infect --ci" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 2b186f2..baae9ba 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -68,6 +68,7 @@ jobs: with: php-version: "${{ matrix.php-version }}" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + tools: castor coverage: "xdebug" - name: "Checkout code" @@ -80,7 +81,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute tests (PHP)" - run: "make ci-cc" + run: "castor test --coverage-text" # - name: Send coverage to Coveralls # if: "matrix.php-version == '8.1' && matrix.dependencies == 'highest'" @@ -102,13 +103,14 @@ jobs: with: php-version: "8.3" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + tools: castor coverage: "none" - name: "Checkout code" uses: "actions/checkout@v3" - name: "Validate Composer configuration" - run: "composer validate --strict" + run: "castor validate" - name: "Install dependencies" uses: "ramsey/composer-install@v3" @@ -116,11 +118,8 @@ jobs: dependency-versions: "highest" composer-options: "--optimize-autoloader" - - name: "Check PSR-4 mapping" - run: "composer dump-autoload --optimize --strict-psr" - - name: "Execute static analysis" - run: "make st" + run: "castor stan" coding_standards: name: "4️⃣ Coding Standards" @@ -134,6 +133,7 @@ jobs: with: php-version: "8.3" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + tools: castor coverage: "none" - name: "Checkout code" @@ -146,11 +146,36 @@ jobs: composer-options: "--optimize-autoloader" - name: "Check coding style" - run: "make ci-cs" + run: "castor cs" - name: "Deptrac" - run: | - vendor/bin/deptrac analyse --fail-on-uncovered --no-cache + run: 'castor deptrac' + + check_licenses: + name: "5️⃣ Check Licenses" + needs: + - "byte_level" + - "syntax_errors" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.3" + extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + tools: castor + + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--optimize-autoloader" + + - name: "Execute license check" + run: "castor check-licenses" rector_checkstyle: name: "6️⃣ Rector Checkstyle" @@ -164,6 +189,7 @@ jobs: with: php-version: "8.3" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + tools: castor coverage: "xdebug" - name: "Checkout code" @@ -179,7 +205,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Rector" - run: "make rector" + run: "castor rector" exported_files: name: "7️⃣ Exported files" diff --git a/.gitignore b/.gitignore index 8da2f5c..ec1249c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ yarn-error.log /composer.lock /vendor infection.txt +/.castor.stub.php diff --git a/castor.php b/castor.php new file mode 100644 index 0000000..8123e27 --- /dev/null +++ b/castor.php @@ -0,0 +1,178 @@ +title('Running infection'); + $nproc = run('nproc', quiet: true); + if (! $nproc->isSuccessful()) { + io()->error('Cannot determine the number of processors'); + return; + } + $threads = (int) $nproc->getOutput(); + $command = [ + 'php', + 'vendor/bin/infection', + sprintf('--min-msi=%s', $minMsi), + sprintf('--min-covered-msi=%s', $minCoveredMsi), + sprintf('--threads=%s', $threads), + ]; + if ($ci) { + $command[] = '--logger-github'; + $command[] = '-s'; + } + $environment = [ + 'XDEBUG_MODE' => 'coverage', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run tests')] +function test(bool $coverageHtml = false, bool $coverageText = false, null|string $group = null): void +{ + io()->title('Running tests'); + $command = ['php', 'vendor/bin/phpunit', '--color']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + if ($coverageHtml) { + $command[] = '--coverage-html=build/coverage'; + $environment['XDEBUG_MODE'] = 'coverage'; + } + if ($coverageText) { + $command[] = '--coverage-text'; + $environment['XDEBUG_MODE'] = 'coverage'; + } + if ($group !== null) { + $command[] = sprintf('--group=%s', $group); + } + run($command, environment: $environment); +} + +#[AsTask(description: 'Coding standards check')] +function cs(bool $fix = false): void +{ + io()->title('Running coding standards check'); + $command = ['php', 'vendor/bin/ecs', 'check']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + if ($fix) { + $command[] = '--fix'; + } + run($command, environment: $environment); +} + +#[AsTask(description: 'Running PHPStan')] +function stan(): void +{ + io()->title('Running PHPStan'); + $command = ['php', 'vendor/bin/phpstan', 'analyse']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Validate Composer configuration')] +function validate(): void +{ + io()->title('Validating Composer configuration'); + $command = ['composer', 'validate', '--strict']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); + + $command = ['composer', 'dump-autoload', '--optimize', '--strict-psr']; + run($command, environment: $environment); +} + +/** + * @param array $allowedLicenses + */ +#[AsTask(description: 'Check licenses')] +function checkLicenses( + array $allowedLicenses = ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT', 'MPL-2.0', 'OSL-3.0'] +): void { + io()->title('Checking licenses'); + $allowedExceptions = []; + $command = ['composer', 'licenses', '-f', 'json']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + $result = run($command, environment: $environment, quiet: true); + if (! $result->isSuccessful()) { + io()->error('Cannot determine licenses'); + exit(1); + } + $licenses = json_decode($result->getOutput(), true); + $disallowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => ! in_array($name, $allowedExceptions, true) + && count(array_diff($info['license'], $allowedLicenses)) === 1, + \ARRAY_FILTER_USE_BOTH + ); + $allowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => in_array($name, $allowedExceptions, true) + || count(array_diff($info['license'], $allowedLicenses)) === 0, + \ARRAY_FILTER_USE_BOTH + ); + if (count($disallowed) > 0) { + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($disallowed), + $disallowed + ) + ); + io() + ->error('Disallowed licenses found'); + exit(1); + } + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($allowed), + $allowed + ) + ); + io() + ->success('All licenses are allowed'); +} + +#[AsTask(description: 'Run Rector')] +function rector(bool $fix = false): void +{ + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/rector', 'process', '--ansi']; + if (! $fix) { + $command[] = '--dry-run'; + } + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run Rector')] +function deptrac(): void +{ + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/deptrac', 'analyse', '--fail-on-uncovered', '--no-cache']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} diff --git a/ecs.php b/ecs.php index 2d4767b..233deaf 100644 --- a/ecs.php +++ b/ecs.php @@ -87,6 +87,12 @@ $config->parallel(); $config->paths([__DIR__]); $config->skip( - [__DIR__ . '/.github', __DIR__ . '/build', __DIR__ . '/vendor', PhpUnitTestClassRequiresCoversFixer::class] + [ + __DIR__ . '/.github', + __DIR__ . '/build', + __DIR__ . '/vendor', + __DIR__ . '/.castor.stub.php', + PhpUnitTestClassRequiresCoversFixer::class, + ] ); }; diff --git a/infection.json.dist b/infection.json similarity index 100% rename from infection.json.dist rename to infection.json