Files
CoreLibs-Composer-All/.github/workflows/php-static-checks-and-tests.yml
2026-07-02 14:29:33 +09:00

372 lines
15 KiB
YAML

# external call not yet setup, only copy to repo at the moment
name: PHP Static Checks and Tests Matrix Workflow
run-name: ${{ github.actor}} runs PHP-MATRIX-CI
on:
workflow_call:
inputs:
php-version:
type: string
required: false
default: '8.5'
description: 'PHP versions to use for the checks and tests, default is 8.4 and 8.5, can be a comma-separated list of versions'
phpunit-version:
type: string
required: false
default: '^13'
description: 'PHPUnit versions to use for the tests, default is ^13, can be a comma-separated list of versions'
operating-system:
type: string
required: false
default: 'ubuntu-latest'
description: 'Operating systems to run the tests on, default is ubuntu-latest, can be a comma-separated list of operating systems'
disable-phan:
type: boolean
default: false
required: false
description: 'Set to "true" to skip running phan'
composer-directory:
type: string
required: false
default: './'
description: 'Directory where composer.json is located, relative to the repository root'
source-directory:
type: string
required: false
default: './src'
description: 'Directory where the PHP source code is located, relative to the repository root'
secrets:
opj-package-registry:
required: false
description: 'Package registry login infos: user|token|url[:user|token|url:...]'
jobs:
setup:
runs-on: ubuntu-latest
outputs:
php-version: ${{ steps.set-matrix.outputs.php-version }}
phpunit-version: ${{ steps.set-matrix.outputs.phpunit-version }}
operating-system: ${{ steps.set-matrix.outputs.operating-system }}
opj-package-registry: ${{ steps.set-matrix.outputs.opj-package-registry }}
steps:
- name: Set matrix values
id: set-matrix
env:
OPJ_PACKAGE_REGISTRY: ${{ secrets.opj-package-registry }}
run: |
echo "php-version=$(echo '${{ inputs.php-version }}' | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | jq -R . | jq -cs .)" >> $GITHUB_OUTPUT
echo "phpunit-version=$(echo '${{ inputs.phpunit-version }}' | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | jq -R . | jq -cs .)" >> $GITHUB_OUTPUT
echo "operating-system=$(echo '${{ inputs.operating-system }}' | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | jq -R . | jq -cs .)" >> $GITHUB_OUTPUT
# take the secrets opj-package-registry, split by ":" for each entry
# then split by "|" for each entry to get user, token, url
# then create a JSON array of objects with user, token, url
if [ -n "$OPJ_PACKAGE_REGISTRY" ]; then
echo "opj-package-registry=$(echo "$OPJ_PACKAGE_REGISTRY" | tr ':' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | jq -R 'split("|") | {user: .[0], token: .[1], url: .[2]}' | jq -cs .)" >> $GITHUB_OUTPUT
else
echo "opj-package-registry=[]" >> $GITHUB_OUTPUT
fi;
php-check-and-test-matrix:
needs: setup
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: ${{fromJSON(needs.setup.outputs.operating-system)}}
php-version: ${{fromJSON(needs.setup.outputs.php-version)}}
phpunit-version: ${{fromJSON(needs.setup.outputs.phpunit-version)}}
# include:
# - operating-system: ubuntu-latest
# php-version: '8.4'
# phpunit-version: '^13'
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl, sqlite, pdo_sqlite
# ini-values: post_max_size=256M, max_execution_time=180
tools: composer, ${{ inputs.disable-phan == false && 'phan, ' || '' }}phpstan, phpcs, phpunit:${{ matrix.phpunit-version }}, cs2pr
# coverage: pcov
coverage: xdebug
- name: Check Tools setup
id: check-tools
run: |
REQUIRED_TOOLS=("composer" "phpunit" "phpstan" ${{ inputs.disable-phan == false && '"phan" ' || '' }}"phpcs" "cs2pr")
SKIP_IF_FAILED=("phan")
for tool in "${REQUIRED_TOOLS[@]}"; do
if ! command -v "$tool" &> /dev/null; then
if [[ " ${SKIP_IF_FAILED[@]} " =~ " ${tool} " ]]; then
echo "⚠️ Warning: $tool is not installed, but it's optional. Skipping..."
echo "${tool}=skip" >> $GITHUB_OUTPUT
continue
fi
echo "❌ Error: $tool failed to load!"
exit 1
else
# the tool path does not have "setup-php/tools/ inside
tool_path=$(which "$tool")
if [[ "$tool_path" != */setup-php/tools/* ]]; then
if [[ " ${SKIP_IF_FAILED[@]} " =~ " ${tool} " ]]; then
echo "⚠️ Warning: $tool installation check failed, but it's optional. Skipping..."
echo "${tool}=skip" >> $GITHUB_OUTPUT
continue
fi
echo "❌ Error: $tool path lookup failed!"
exit 1
fi
tool_version=$("$tool" --version 2>&1)
echo "✅ $tool loaded successfully: $tool_version ($(which $tool))"
echo "${tool}=ok" >> $GITHUB_OUTPUT
fi
done
- name: Check source folder and composer.json setup
run: |
if [ ! -d "${{ inputs.source-directory }}" ]; then
echo "❌ Error: Source directory '${{ inputs.source-directory }}' does not exist!"
exit 1
fi
if [ ! -f "${{ inputs.composer-directory }}/composer.json" ]; then
echo "❌ Error: composer.json not found in '${{ inputs.composer-directory }}'!"
exit 1
fi
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
working-directory: ${{ inputs.composer-directory }}
- name: Cache dependencies
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Authenticate to private package repository
env:
OPJ_PACKAGE_REGISTRY: ${{ needs.setup.outputs.opj-package-registry }}
run: |
# opj-package-registry is a JSON array of objects with user, token, url
for entry in $(echo "$OPJ_PACKAGE_REGISTRY" | jq -c '.[]'); do
user=$(echo "$entry" | jq -r '.user')
token=$(echo "$entry" | jq -r '.token')
url=$(echo "$entry" | jq -r '.url')
# if user, token or url is empty, skip this entry
if [ -z "$user" ] || [ -z "$token" ] || [ -z "$url" ]; then
echo "⚠️ Warning: Skipping invalid registry entry: $entry"
continue
fi
echo "Authenticating to private package repository: $url with user: $user"
composer config http-basic."$url" "$user" "$token"
done
composer install --prefer-dist
working-directory: ${{ inputs.composer-directory }}
- name: Run composer audit
id: run-composer-audit
run: composer audit --format=json --no-scripts --no-plugins --no-interaction > composer-audit-output.json
# some post action with the composer output format file
working-directory: ${{ inputs.composer-directory }}
- name: Run phpcs
id: run-phpcs
run: phpcs -q --report=checkstyle --standard=phpcs.xml --ignore=vendor/ ${{ inputs.source-directory }} | cs2pr
- name: Run phan
id: run-phan
if: ${{ ! inputs.disable-phan && steps.check-tools.outputs.phan == 'ok' }}
run: phan --analyze-twice --no-progress-bar --config-file=./.phan/config.php
# Must have "./tmp" directory set in configuration
# if we have "./tmp/phpstan" or anything different adjust the path below
- name: "Restore result cache"
uses: actions/cache/restore@v5
with:
path: ./tmp
key: "result-cache-${{ github.run_id }}"
restore-keys: |
result-cache-v1-
- name: Run PHPStan
id: run-phpstan
run: phpstan analyse --no-progress --configuration=phpstan.neon
- name: "Save result cache"
uses: actions/cache/save@v5
if: always()
with:
path: ./tmp
key: "result-cache-${{ github.run_id }}"
- name: Test with phpunit
id: run-phpunit
run: |
# if we are older than 12 there is no openclover
# version can start with ^ and then have numbers, so we need to remove the ^ and then compare the version numbers
phpunit_version="${{ matrix.phpunit-version }}"
phpunit_version="${phpunit_version#^}"
phpunit_version="${phpunit_version%%.*}"
if [ "$phpunit_version" -lt 12 ]; then
phpunit --colors=never \
--no-coverage \
--coverage-text=phpunit-coverage.txt \
--coverage-clover=phpunit-openclover.xml \
--log-junit=phpunit-junit.log
else
phpunit --colors=never \
--no-coverage \
--coverage-text=phpunit-coverage.txt \
--only-summary-for-coverage-text \
--coverage-openclover=phpunit-openclover.xml \
--log-junit=phpunit-junit.log \
--log-otr=phpunit-otr.log
fi;
- name: Code Coverage Summary Report
uses: saschanowak/CloverCodeCoverageSummary@1.1.1
with:
filename: phpunit-openclover.xml
- name: Summary
if: always()
env:
COMPOSER_AUDIT_RESULT: ${{ steps.run-composer-audit.outcome }}
COMPOSER_AUDIT_FILE: ${{ inputs.composer-directory }}/composer-audit-output.json
PHPCS_RESULT: ${{ steps.run-phpcs.outcome }}
PHAN_RESULT: ${{ steps.run-phan.outcome }}
PHPSTAN_RESULT: ${{ steps.run-phpstan.outcome }}
PHPUNIT_RESULT: ${{ steps.run-phpunit.outcome }}
run: |
icon() { if [[ "$1" == "success" ]]; then echo "✅"; elif [[ "$1" == "skipped" ]]; then echo "⏭️"; else echo "❌"; fi; }
row() { echo "<tr><td><b>$1</b></td><td>$(icon $2) $2</td></tr>"; }
cat >> $GITHUB_STEP_SUMMARY << EOF
<h2>PHP CI Results</h2>
<table>
<thead>
<tr><th>Check</th><th>Status</th></tr>
</thead>
<tbody>
$(row "composer audit" "$COMPOSER_AUDIT_RESULT")
$(row "phpcs" "$PHPCS_RESULT")
$(row "phan" "$PHAN_RESULT")
$(row "phpstan" "$PHPSTAN_RESULT")
$(row "phpunit" "$PHPUNIT_RESULT")
</tbody>
</table>
<details>
<summary>Run details</summary>
| Input | Value |
| --- | --- |
| PHP version | \`${{ inputs.php-version }}\` |
| PHPUnit version | \`${{ inputs.phpunit-version }}\` |
| Operating system | \`${{ inputs.operating-system }}\` |
| Phan disabled | \`${{ inputs.disable-phan }}\` |
| Composer directory | \`${{ inputs.composer-directory }}\` |
| Source directory | \`${{ inputs.source-directory }}\` |
</details>
EOF
if [ -f "$COMPOSER_AUDIT_FILE" ] && jq empty "$COMPOSER_AUDIT_FILE" 2>/dev/null; then
{
echo "<h3>Composer Audit</h3>"
echo ""
PACKAGES_AUDITED=$(jq -r '.metadata["packages-audited"] // "?"' "$COMPOSER_AUDIT_FILE")
ADVISORIES_COUNT=$(jq -r '.metadata["packages-with-advisories"] // 0' "$COMPOSER_AUDIT_FILE")
echo "| Packages audited | With advisories |"
echo "| --- | --- |"
echo "| $PACKAGES_AUDITED | $ADVISORIES_COUNT |"
echo ""
ADVISORY_ROWS=$(jq -r '
.packages[]
| select(.advisories and (.advisories | length) > 0)
| .name as $name | .version as $ver
| .advisories[]
| "| \($name) | \($ver) | \(.cve // "N/A") | \(.title) |"
' "$COMPOSER_AUDIT_FILE" 2>/dev/null || true)
if [ -n "$ADVISORY_ROWS" ]; then
echo "| Package | Version | CVE | Title |"
echo "| --- | --- | --- | --- |"
echo "$ADVISORY_ROWS"
echo ""
fi
echo "<details><summary>Full audit output</summary>"
echo ""
echo '```json'
cat "$COMPOSER_AUDIT_FILE"
echo '```'
echo ""
echo "</details>"
} >> $GITHUB_STEP_SUMMARY
fi
if [ -f phpunit-junit.log ]; then
{
echo "<h3>PHPUnit Test Results</h3>"
echo ""
# Extract totals from the top-level testsuite element (first testsuite line)
JUNIT_LINE=$(grep -m1 '<testsuite ' phpunit-junit.log)
TESTS=$(echo "$JUNIT_LINE" | grep -oP 'tests="\K[0-9]+')
ASSERTIONS=$(echo "$JUNIT_LINE" | grep -oP 'assertions="\K[0-9]+')
ERRORS=$(echo "$JUNIT_LINE" | grep -oP 'errors="\K[0-9]+')
FAILURES=$(echo "$JUNIT_LINE" | grep -oP 'failures="\K[0-9]+')
SKIPPED=$(echo "$JUNIT_LINE" | grep -oP 'skipped="\K[0-9]+')
TIME=$(echo "$JUNIT_LINE" | grep -oP 'time="\K[0-9]+\.[0-9]+')
PASSED=$(( TESTS - ERRORS - FAILURES - SKIPPED ))
test_icon() {
if [ "$1" -gt 0 ]; then echo "❌"; else echo "✅"; fi
}
echo "| Metric | Value |"
echo "| --- | --- |"
echo "| Tests | $TESTS |"
echo "| Passed | $PASSED |"
echo "| Assertions | $ASSERTIONS |"
echo "| Skipped | $(test_icon "${SKIPPED:-0}") ${SKIPPED:-0} |"
echo "| Failures | $(test_icon "${FAILURES:-0}") ${FAILURES:-0} |"
echo "| Errors | $(test_icon "${ERRORS:-0}") ${ERRORS:-0} |"
echo "| Time | ${TIME}s |"
echo ""
} >> $GITHUB_STEP_SUMMARY
fi
if [ -f phpunit-coverage.txt ]; then
{
echo "<h3>Code Coverage</h3>"
echo ""
echo "| Metric | Coverage | Status |"
echo "| --- | --- | --- |"
grep -E '^\s+(Classes|Methods|Lines):' phpunit-coverage.txt | head -3 | while IFS= read -r line; do
name=$(echo "$line" | sed 's/^[[:space:]]*//' | cut -d: -f1)
value=$(echo "$line" | cut -d: -f2- | sed 's/^[[:space:]]*//')
pct=$(echo "$value" | grep -oE '[0-9]+\.[0-9]+' | head -1)
circle=$(awk -v p="$pct" 'BEGIN { if (p+0 == 100) print "🟢"; else if (p+0 >= 60) print "🟠"; else print "🔴" }')
echo "| $name | $value | $circle |"
done
echo ""
echo "<details><summary>Full coverage report</summary>"
echo ""
echo '```'
cat phpunit-coverage.txt
echo '```'
echo ""
echo "</details>"
} >> $GITHUB_STEP_SUMMARY
fi
if [ -f phpunit-openclover.xml ]; then
if [ -f code-coverage-summary.md ]; then
cat code-coverage-summary.md >> $GITHUB_STEP_SUMMARY
fi
if [ -f code-coverage-details.md ]; then
cat code-coverage-details.md >> $GITHUB_STEP_SUMMARY
fi
fi