Skip to content

Commit 700cc32

Browse files
committed
feat: add php version param for quality tools
1 parent d507c77 commit 700cc32

File tree

2 files changed

+88
-25
lines changed

2 files changed

+88
-25
lines changed

.github/workflows/symfony-php-reusable.yml

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ on:
8888
required: false
8989
type: string
9090
default: 'coverage.xml'
91+
phpcov-version:
92+
description: 'phpcov PHAR version to download from phar.phpunit.de (e.g. 8.2 -> phpcov-8.2.phar)'
93+
required: false
94+
type: string
95+
default: '8.2'
9196
phpstan-level:
9297
description: 'PHPStan level (0-9 or max)'
9398
required: false
@@ -476,17 +481,48 @@ jobs:
476481
combinations.append(include_item)
477482
478483
# Filter out excluded combinations
484+
def _normalize_version(val: str):
485+
"""
486+
Normalize versions so that values with trailing .* behave as a prefix filter.
487+
Examples:
488+
rule: "7.3.*" matches combo values "7.3", "7.3.*", "7.3.x", etc. (prefix "7.3")
489+
rule: "7.3" requires exact match "7.3" (no wildcard semantics)
490+
"""
491+
return val[:-2] if isinstance(val, str) and val.endswith('.*') else val
492+
493+
def _matches(rule_val, combo_val):
494+
# Exact match when rule has no wildcard
495+
if not (isinstance(rule_val, str) and rule_val.endswith('.*')):
496+
return combo_val == rule_val
497+
498+
# Wildcard/prefix match when rule ends with .* (treat as startswith of the prefix)
499+
prefix = _normalize_version(rule_val)
500+
501+
if not isinstance(combo_val, str):
502+
return False
503+
504+
# Also normalize combo like "7.3.*" to its prefix for matching consistency
505+
combo_norm = _normalize_version(combo_val)
506+
return str(combo_norm).startswith(str(prefix))
507+
479508
def should_exclude(combo, exclude_rules):
480509
for rule in exclude_rules:
481-
if all(combo.get(k) == v for k, v in rule.items()):
510+
# All keys in rule must match the combo using wildcard-aware semantics
511+
if all(_matches(v, combo.get(k)) for k, v in rule.items()):
482512
return True
483513
return False
484514
485-
final_combinations = [c for c in combinations if not should_exclude(c, matrix_exclude)]
515+
excluded = []
516+
final_combinations = []
517+
for c in combinations:
518+
if should_exclude(c, matrix_exclude):
519+
excluded.append(c)
520+
else:
521+
final_combinations.append(c)
486522
487523
# Output as JSON
488524
matrix_json = json.dumps({'include': final_combinations})
489-
print(f"Generated {len(final_combinations)} test combinations")
525+
print(f"Generated {len(final_combinations)} test combinations (excluded {len(excluded)})")
490526
491527
# Write to GITHUB_OUTPUT
492528
import os
@@ -598,14 +634,19 @@ jobs:
598634
599635
- name: Install dependencies
600636
working-directory: ${{ inputs['working-directory'] }}
637+
env:
638+
# Pass detected Symfony version to Composer/Symfony Flex, if any
639+
SYMFONY_REQUIRE: ${{ steps.apply-constraints.outputs.symfony_version }}
601640
run: |
602-
echo "Requested constraints: ${{ steps.apply-constraints.outputs.composer_packages }}"
603-
if [ "${{ matrix.dependencies }}" = "lowest" ]; then
604-
echo "Installing lowest compatible dependencies..."
605-
composer update --prefer-lowest --prefer-stable --no-interaction --no-progress
641+
# Make sure Flex is allowed/available (if not already in your workflow)
642+
composer global config --no-plugins allow-plugins.symfony/flex true
643+
composer global require --no-progress --no-scripts --no-plugins symfony/flex
644+
if [ -n "${SYMFONY_REQUIRE}" ]; then
645+
echo "Using Symfony constraint: ${SYMFONY_REQUIRE}"
646+
composer update --prefer-dist --no-progress --no-interaction
606647
else
607-
echo "Installing highest (locked) dependencies..."
608-
composer update --no-interaction --no-progress --prefer-dist
648+
echo "No SYMFONY_REQUIRE set, using lock file"
649+
composer install --prefer-dist --no-progress --no-interaction
609650
fi
610651
611652
- name: Run PHPUnit
@@ -661,11 +702,19 @@ jobs:
661702
php-version: ${{ inputs['php-version-quality-tools'] }}
662703
coverage: none
663704

664-
- name: Install phpcov
705+
- name: Install coverage tools (phpcov PHAR + phpunit-coverage-check)
665706
run: |
707+
set -e
708+
# Download phpcov PHAR for requested version
709+
PHPCOV_VERSION="${{ inputs['phpcov-version'] }}"
710+
PHPCOV_URL="https://phar.phpunit.de/phpcov-${PHPCOV_VERSION}.phar"
711+
echo "Downloading phpcov from ${PHPCOV_URL}"
712+
wget -q -O phpcov.phar "$PHPCOV_URL"
713+
chmod +x phpcov.phar
714+
715+
# Install phpunit-coverage-check globally via Composer
666716
composer global require --no-interaction --no-progress \
667-
phpcov/phpcov:^8 \
668-
rregeer/phpunit-coverage-check:^1.0
717+
rregeer/phpunit-coverage-check:*
669718
# Add Composer global bin to PATH for both Composer 1 and 2 locations
670719
echo "$HOME/.composer/vendor/bin" >> $GITHUB_PATH
671720
echo "$HOME/.config/composer/vendor/bin" >> $GITHUB_PATH
@@ -702,7 +751,7 @@ jobs:
702751
703752
# Try merging with phpcov; if it fails, we will fallback later
704753
set +e
705-
phpcov merge --clover merged-coverage.xml "${COVERAGE_DIRS[@]}"
754+
php phpcov.phar merge --clover merged-coverage.xml "${COVERAGE_DIRS[@]}"
706755
EXIT_CODE=$?
707756
set -e
708757
@@ -715,7 +764,7 @@ jobs:
715764
echo "phpcov merge succeeded"
716765
echo "merged=true" >> $GITHUB_OUTPUT
717766
718-
- name: Parse coverage and validate threshold (phpunit-coverage-check)
767+
- name: Parse coverage and validate threshold (coverage-check)
719768
id: coverage
720769
env:
721770
COVERAGE_THRESHOLD: ${{ inputs['coverage-threshold'] }}
@@ -743,16 +792,16 @@ jobs:
743792
exit 0
744793
fi
745794
746-
# Run phpunit-coverage-check. If multiple files, record minimum coverage and mark failure if any fail.
795+
# Run coverage-check (from rregeer/phpunit-coverage-check). If multiple files, record minimum coverage and mark failure if any fail.
747796
OVERALL_STATUS=success
748797
MIN_COVERAGE=""
749798
for FILE in "${TARGETS[@]}"; do
750799
set +e
751-
phpunit-coverage-check --min="${COVERAGE_THRESHOLD}" "$FILE" | tee coverage_check_output.txt
800+
coverage-check "$FILE" "${COVERAGE_THRESHOLD}" | tee coverage_check_output.txt
752801
EXIT_CODE=$?
753802
set -e
754-
# Extract percentage like 85.23 from the tool output (last occurrence)
755-
PCT=$(sed -nE 's/.*([0-9]+\.[0-9]+)%.*$/\1/p' coverage_check_output.txt | tail -n1)
803+
# Extract the current coverage percentage (first percentage in output)
804+
PCT=$(grep -Eo '([0-9]+(\.[0-9]+)?)%' coverage_check_output.txt | head -n1 | tr -d '%')
756805
if [ -z "$PCT" ]; then
757806
# Fallback: try to read line-rate from Clover and convert to percent
758807
LR=$(grep -o 'line-rate="[0-9.]*"' "$FILE" | head -1 | sed -E 's/.*="([0-9.]+)"/\1/')
@@ -782,8 +831,8 @@ jobs:
782831
<!-- coverage-validation-report -->
783832
## 📊 Code Coverage Report
784833
${{ steps.coverage.outputs.status == 'success'
785-
&& format('✅Coverage: {0}% (min {1}%) — OK', steps.coverage.outputs.coverage, inputs['coverage-threshold'])
786-
|| format('❌Coverage: {0}% (min {1}%) — NOT OK', steps.coverage.outputs.coverage, inputs['coverage-threshold'])
834+
&& format('✅ Coverage: {0}% (min {1}%) — OK', steps.coverage.outputs.coverage, inputs['coverage-threshold'])
835+
|| format('❌ Coverage: {0}% (min {1}%) — NOT OK', steps.coverage.outputs.coverage, inputs['coverage-threshold'])
787836
}}
788837
789838
---

docs/workflows/symfony-php-reusable.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The workflow now features **fully dynamic matrix generation** with Cartesian pro
1717
- 🚫 **matrix-exclude**: Filter incompatible combinations
1818
- 🐛 **Xdebug Auto-Configuration**: Automatically adds xdebug extension when coverage enabled
1919
- 🔧 **Quality Tools PHP Version**: Configure separate PHP version for static analysis tools
20+
- 🧩 **Symfony Minor Detection**: The workflow detects the Symfony version from your matrix and exposes it to Composer via `SYMFONY_REQUIRE` during dependency installation (no change to install commands required)
2021

2122
**Example Matrix Output:**
2223
```text
@@ -69,11 +70,12 @@ This workflow automates the complete CI process for Symfony PHP projects:
6970
| `enable-coverage-check` | boolean || `false` | Enable coverage validation and PR comments |
7071
| `coverage-threshold` | number || `70` | Minimum code coverage percentage required (0-100) |
7172
| `coverage-file` | string || `coverage.xml` | Coverage report file name (Clover XML). Used for PHPUnit generation, Codecov upload, and coverage validation. |
73+
| `phpcov-version` | string || `8.2` | Version of phpcov PHAR to download from phar.phpunit.de (e.g., `8.2` downloads `phpcov-8.2.phar`). |
7274
| `phpstan-level` | string || `max` | PHPStan level (0-9 or max) |
7375
| `working-directory` | string || `.` | Working directory for the project |
7476
| `php-extensions` | string || `mbstring, json` | Additional PHP extensions to install. xdebug auto-added when coverage enabled. |
7577
| `php-version-quality-tools` | string || `8.3` | PHP version to use for quality tools (PHPStan, PHPCS, Rector, Infection, etc.) |
76-
| `matrix-exclude` | string (JSON array) || Default Symfony 7.x exclusions | Matrix combinations to exclude. Supports any package from `versions-matrix`. |
78+
| `matrix-exclude` | string (JSON array) || Default Symfony 7.x exclusions | Matrix combinations to exclude. Supports any package from `versions-matrix`. Values ending with `.*` act as wildcards/prefixes (e.g., `"7.3.*"` matches `"7.3"` and `"7.3.*"`), while values without `.*` require exact match. |
7779
| `infection-min-msi` | number || `80` | Minimum Mutation Score Indicator (MSI) for Infection |
7880
| `infection-min-covered-msi` | number || `90` | Minimum Covered Code MSI for Infection |
7981
| `phpstan-config` | string || `''` | Path to PHPStan configuration file (default: phpstan.neon.dist or phpstan.neon) |
@@ -716,7 +718,7 @@ Generated Combinations:
716718
- **Symfony 7.3+**: Requires PHP 8.2+
717719

718720
#### Dynamic Package Constraint Application
719-
The workflow automatically applies version constraints for **all packages** in versions-matrix:
721+
The workflow automatically applies version constraints for **all packages** in versions-matrix and detects Symfony minor for Flex:
720722

721723
**Process:**
722724
1. **Parse Matrix**: Each test job receives matrix combination (e.g., `{"php": "8.3", "symfony/cache": "7.0.*", "symfony/config": "6.4.*"}`)
@@ -726,6 +728,7 @@ The workflow automatically applies version constraints for **all packages** in v
726728
- Apply constraint: `composer require "{package}:{version}" --no-update --no-interaction`
727729
- Detect Symfony version and set Composer extra: If the package is `symfony/framework-bundle`, set `composer config extra.symfony.require "{version}"`. If not provided, the workflow will try to infer Symfony version from the first `symfony/*` package in the matrix and set it accordingly.
728730
4. **Install Dependencies**:
731+
- The job exports `SYMFONY_REQUIRE` environment variable for this step using the detected Symfony version. This lets Symfony Flex align all `symfony/*` packages to the same minor automatically.
729732
- Highest: `composer update --no-interaction --no-progress --prefer-dist`
730733
- Lowest: `composer update --prefer-lowest --prefer-stable --no-interaction --no-progress`
731734

@@ -754,6 +757,7 @@ versions-matrix: |
754757
# - symfony/console:7.0.*
755758
# - doctrine/orm:3.0.*
756759
# extra.symfony.require: 7.0.*
760+
# SYMFONY_REQUIRE env (Install dependencies step): 7.0.*
757761
```
758762

759763
**Key Feature**: NO hardcoded package names! The workflow dynamically detects and applies constraints for any packages in the matrix.
@@ -836,6 +840,16 @@ with:
836840
]
837841
```
838842

843+
#### Exclusion Matching Semantics
844+
845+
- Wildcard match: A rule value ending with `.*` is treated as a prefix. For example:
846+
- `{ "symfony/framework-bundle": "7.3.*" }` will match matrix values `"7.3"`, `"7.3.*"`, `"7.3.x"`, etc.
847+
- Exact match: A rule value without `.*` must equal the matrix value exactly. For example:
848+
- `{ "php": "8.2" }` matches only `"8.2"`, not `"8.2.1"`.
849+
- Mixed rules: All keys in a rule must match (AND behavior). This lets you target precise combinations like `{ "php": "8.2", "symfony/framework-bundle": "7.2.*" }`.
850+
851+
This behavior ensures that exclusions like `"7.2.*"` and `"7.3.*"` properly skip jobs even if your `versions-matrix` lists values as `"7.2"` or `"7.3"` without the trailing `.*`.
852+
839853
**Disable All Exclusions:**
840854
```yaml
841855
with:
@@ -952,9 +966,9 @@ coverage-threshold: 80 # Minimum coverage % (default: 70)
952966
1. Unit tests with coverage generate Clover XML (`coverage.xml` by default) and raw coverage (`coverage.cov`).
953967
2. Coverage files are uploaded as artifacts (one artifact per matrix job).
954968
3. `coverage-validation` job:
955-
- Installs two tools globally: `phpcov/phpcov` and `rregeer/phpunit-coverage-check`
969+
- Downloads `phpcov` as a PHAR from phar.phpunit.de using `phpcov-version` input and installs `rregeer/phpunit-coverage-check` globally
956970
- Downloads all coverage artifacts to `coverage-artifacts/`
957-
- Attempts to merge raw coverage with `phpcov merge --clover merged-coverage.xml <dirs>`
971+
- Attempts to merge raw coverage with `php phpcov.phar merge --clover merged-coverage.xml <dirs>`
958972
- Runs `phpunit-coverage-check --min=<threshold>` on the merged Clover; if no merged file exists, checks the single Clover file; if multiple Clovers exist, each is checked and the result fails if any are below the threshold
959973
4. Posts or updates a PR comment with a simple OK/NOT OK message and the current coverage value
960974
5. Fails job if coverage < threshold
@@ -968,7 +982,7 @@ Coverage: 85.50% (min 80%) — OK
968982
- Must run on `pull_request` event (validation only runs on PRs)
969983
- At least one matrix combination must have `"coverage": "xdebug"`
970984
- Requires `pull-requests: write` permission (auto-granted in most workflows)
971-
- `phpcov/phpcov` and `rregeer/phpunit-coverage-check` are installed automatically in the coverage validation job
985+
- `phpcov` is downloaded as a PHAR (`phpcov-<version>.phar`) and `rregeer/phpunit-coverage-check` is installed automatically in the coverage validation job
972986

973987
**Edge Cases:**
974988
- **No coverage artifacts**: Job succeeds with status "no-coverage" (no comment posted)

0 commit comments

Comments
 (0)