Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 117 additions & 13 deletions .github/workflows/plugin-phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ on:
type: string
default: ''
php-version:
description: "PHP version the job runs on; the analysis level and phpVersion come from the plugin's phpstan.neon"
description: "PHP version the job runs on; the analysis level and phpVersion come from the plugin's phpstan.neon. Accepts a literal version or one of the shared aliases (matomo5_min_php, matomo5_max_php, matomo6_min_php, matomo6_max_php), which track a Matomo major's floor and ceiling centrally."
required: false
type: string
default: '7.2'
default: 'matomo5_min_php'
scripts-ref:
description: "Ref of matomo-org/github-action-tests to take the helper scripts from. When pinning the workflow to a SHA, pass the same SHA here."
required: false
Expand All @@ -27,6 +27,16 @@ on:
required: false
type: boolean
default: true
matomo-targets:
description: >-
JSON array of {target, php} objects, one per analysis run. Analysing the minimum target
is what catches a plugin calling a core API that does not exist yet in the oldest Matomo
its plugin.json supports. `php` is optional and falls back to the php-version input; set
it per target when the targets span Matomo majors, because a Matomo 6 checkout cannot be
bootstrapped by the PHP 7.2 that Matomo 5 allows.
required: false
type: string
default: '[{"target": "minimum_required_matomo"}, {"target": "maximum_supported_matomo"}]'
secrets:
TESTS_ACCESS_TOKEN:
required: false
Expand All @@ -38,8 +48,12 @@ permissions:

jobs:
phpstan:
name: PHPStan
name: PHPStan (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(inputs.matomo-targets) }}
env:
PLUGIN_NAME: ${{ inputs.plugin-name }}
DEPENDENT_PLUGINS: ${{ inputs.dependent-plugins }}
Expand All @@ -55,14 +69,11 @@ jobs:
with:
lfs: false
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ inputs.php-version }}

# No GitHub context identifies the called workflow's own commit (verified
# empirically: github.job_workflow_sha evaluates to empty), so pinning the
# scripts is the caller's job via scripts-ref.
# scripts is the caller's job via scripts-ref. This runs before PHP is set up
# because the version aliases are resolved by one of those scripts.
- name: Check out github-action-tests repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
Expand All @@ -71,6 +82,23 @@ jobs:
path: github-action-tests
persist-credentials: false

- name: Resolve PHP version
id: resolve-php
shell: bash
env:
# per-target when the targets span Matomo majors, else the workflow-wide input
PHP_VERSION_INPUT: ${{ matrix.php || inputs.php-version }}
run: |
RESOLVED_VERSION=$("${{ github.workspace }}/github-action-tests/scripts/bash/resolve_php_version.sh" "$PHP_VERSION_INPUT")

echo "Resolved php-version '$PHP_VERSION_INPUT' to '$RESOLVED_VERSION'"
echo "version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ steps.resolve-php.outputs.version }}

- name: Check the pre-push hook matches the canonical copy
if: ${{ inputs.verify-hook }}
shell: bash
Expand All @@ -82,7 +110,45 @@ jobs:
env:
WORKSPACE: ${{ github.workspace }}
ACTION_PATH: ${{ github.workspace }}/github-action-tests
MATOMO_TEST_TARGET: maximum_supported_matomo
MATOMO_TEST_TARGET: ${{ matrix.target }}

- name: Check the PHP version can bootstrap this Matomo
shell: bash
working-directory: ${{ github.workspace }}/matomo
env:
MATOMO_TARGET: ${{ matrix.target }}
run: |
# bootstrap-phpstan.php executes core rather than only parsing it, so analysing a
# Matomo 6 checkout under the PHP 7.2 that Matomo 5 allows dies on 8.1 syntax partway
# through. Catch it here, where the message can say what to change.
MATOMO_MIN_PHP=$(grep -oE "piwik_minimumPHPVersion = '[0-9.]+'" core/testMinimumPhpVersion.php | grep -oE "[0-9]+\.[0-9.]+" | head -1)

if [ -z "$MATOMO_MIN_PHP" ]; then
echo "Could not read piwik_minimumPHPVersion; skipping the compatibility check."
exit 0
fi

if ! php -r 'exit(version_compare(PHP_VERSION, $argv[1], ">=") ? 0 : 1);' "$MATOMO_MIN_PHP"; then
echo "::error::Matomo target '$MATOMO_TARGET' requires PHP >= $MATOMO_MIN_PHP but this job runs $(php -r 'echo PHP_VERSION;'). Give this target its own PHP in the matomo-targets input, for example {\"target\": \"$MATOMO_TARGET\", \"php\": \"matomo6_min_php\"}."
exit 1
fi

echo "PHP $(php -r 'echo PHP_VERSION;') satisfies this Matomo's floor of $MATOMO_MIN_PHP."

- name: Provide the PHPStan bootstrap when Matomo predates it
shell: bash
working-directory: ${{ github.workspace }}/matomo
run: |
if [ -f bootstrap-phpstan.php ]; then
echo "Matomo provides bootstrap-phpstan.php."
exit 0
fi
# Matomo only ships this file from 5.4.0, and plugin phpstan.neon files reference it
# as ../../bootstrap-phpstan.php, so without it PHPStan exits before analysing
# anything on older targets. It has to be copied into the Matomo root rather than
# referenced in place: it derives PIWIK_DOCUMENT_ROOT from its own location.
cp "${{ github.workspace }}/github-action-tests/artifacts/bootstrap-phpstan.php" bootstrap-phpstan.php
echo "Matomo predates bootstrap-phpstan.php; using the copy from github-action-tests."

- name: prepare setup
shell: bash
Expand All @@ -91,6 +157,21 @@ jobs:
echo -e "composer install"
composer install --ignore-platform-reqs

- name: Provide PHPStan itself when Matomo predates it
shell: bash
working-directory: ${{ github.workspace }}/matomo
run: |
if [ -x vendor/bin/phpstan ]; then
echo "Matomo provides PHPStan."
exit 0
fi
# Matomo gained phpstan/phpstan, the composer `phpstan` script and
# bootstrap-phpstan.php in one commit, released in 5.4.0. On an older target there
# is no analyser at all, so install the same constraint that Matomo declares rather
# than the newest release, whose own findings would differ from the max leg's.
echo "Matomo predates phpstan/phpstan; installing ~1.12 to analyse against it."
composer require --dev --ignore-platform-reqs --no-interaction "phpstan/phpstan:~1.12"

- name: checkout additional plugins
if: ${{ inputs.dependent-plugins != '' }}
shell: bash
Expand All @@ -104,17 +185,40 @@ jobs:
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: /tmp/phpstan # same as in phpstan.neon
key: "phpstan-result-cache-${{ github.run_id }}"
# the target is part of the key because the legs analyse different Matomo trees and
# both save within one run, which would otherwise collide on a single key
key: "phpstan-result-cache-${{ matrix.target }}-${{ github.run_id }}"
restore-keys: |
phpstan-result-cache-
phpstan-result-cache-${{ matrix.target }}-

- name: PHPStan whole repo
id: phpstan-all
run: cd ${{ github.workspace }}/matomo && composer run phpstan -- -vvv -c "plugins/${PLUGIN_NAME}/phpstan.neon"
shell: bash
working-directory: ${{ github.workspace }}/matomo
env:
MATOMO_TARGET: ${{ matrix.target }}
# not `composer run phpstan`: that script arrived with PHPStan itself in 5.4.0, so it
# does not exist on older targets. The binary is equivalent — the script is only
# `phpstan analyse -c phpstan.neon`, whose config this command overrides anyway.
run: |
CONFIG="plugins/${PLUGIN_NAME}/phpstan.neon"

# A plugin may guard a newer core API behind class_exists and still be analysed
# against an older Matomo that lacks it, which PHPStan cannot see through. An
# optional phpstan-min-matomo.neon holds those ignores. It applies to this leg only:
# on the maximum leg the error does not occur, so the same ignore would be unmatched
# and reportUnmatchedIgnoredErrors would fail that leg instead.
if [ "$MATOMO_TARGET" = 'minimum_required_matomo' ] \
&& [ -f "plugins/${PLUGIN_NAME}/phpstan-min-matomo.neon" ]; then
CONFIG="plugins/${PLUGIN_NAME}/phpstan-min-matomo.neon"
echo "Using the plugin's minimum-target config: $CONFIG"
fi

vendor/bin/phpstan analyse -vvv -c "$CONFIG"

- name: "Save result cache"
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
if: ${{ !cancelled() }}
with:
path: /tmp/phpstan # same as in phpstan.neon
key: "phpstan-result-cache-${{ github.run_id }}"
key: "phpstan-result-cache-${{ matrix.target }}-${{ github.run_id }}"
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ This action is able to run certain test suites for Matomo or any Matomo plugin.

Defines the PHP version to set up for testing. (Not needed for Client tests)

Use `matomo5_min_php`/`matomo5_max_php` (Matomo 5) or `matomo6_min_php`/`matomo6_max_php` (Matomo 6) to resolve to the centrally managed (defined in action.yml) minimum or maximum PHP versions supported by Matomo tests.
Use `matomo5_min_php`/`matomo5_max_php` (Matomo 5) or `matomo6_min_php`/`matomo6_max_php` (Matomo 6) to resolve to the centrally managed minimum or maximum PHP versions supported by Matomo tests. The alias table is `scripts/bash/resolve_php_version.sh`, shared with the reusable PHPStan workflow so the two cannot drift apart.

The action uses `shivammathur/setup-php` to set up PHP. You can find supported PHP versions here: https://github.com/shivammathur/setup-php#tada-php-support

Expand Down Expand Up @@ -171,7 +171,7 @@ jobs:
with:
plugin-name: MyPlugin
# dependent-plugins: 'innocraft/plugin-Funnels'
# php-version: '8.2'
# php-version: matomo5_min_php # or a literal version such as '8.2'
secrets:
TESTS_ACCESS_TOKEN: ${{ secrets.TESTS_ACCESS_TOKEN }}
```
Expand All @@ -181,6 +181,56 @@ scripts. For immutability, pin the `uses:` reference to a full commit SHA — re
mutable unless the repository enforces immutable releases — and pass the same SHA as
`scripts-ref`, which the workflow uses to check out its helper scripts (default: `main`).

The analysis runs once per entry in `matomo-targets`, which defaults to the minimum Matomo the
plugin's `plugin.json` declares and the maximum it supports. The minimum leg is the one that
catches a plugin calling a core API that does not exist yet in the oldest Matomo it claims to
support — a call that analyses cleanly against current core and then fatals for those users.
Pass a single-element array to analyse against one target:

```yaml
matomo-targets: '[{"target": "maximum_supported_matomo"}]'
```

Each entry may carry its own `php`, falling back to the `php-version` input when it does not.
Set it per target once a plugin's targets span Matomo majors: `bootstrap-phpstan.php` executes
core rather than only parsing it, so a Matomo 6 checkout cannot be bootstrapped by the PHP 7.2
that Matomo 5 still allows. The workflow compares the resolved PHP against the checked-out
Matomo's `piwik_minimumPHPVersion` and fails with that instruction rather than dying on 8.1
syntax partway through the analysis.

```yaml
matomo-targets: >-
[{"target": "minimum_required_matomo", "php": "matomo5_min_php"},
{"target": "maximum_supported_matomo", "php": "matomo6_min_php"}]
```

Matomo gained `phpstan/phpstan`, the composer `phpstan` script and `bootstrap-phpstan.php` in a
single commit, released in 5.4.0. On an older target none of them exist, so the workflow copies
`artifacts/bootstrap-phpstan.php` into the Matomo root, installs the `~1.12` PHPStan that Matomo
itself declares, and runs `vendor/bin/phpstan` rather than the composer script. That is what
keeps the minimum leg usable for the many plugins whose declared floor predates 5.4.0.

A plugin may legitimately call a core API that its minimum Matomo does not have, guarded by
`class_exists` so the path is unreachable there. PHPStan cannot see through that guard and
reports the call on the minimum leg. Put those ignores in an optional
`plugins/<Plugin>/phpstan-min-matomo.neon`, which the workflow uses **for the minimum leg only**:

```neon
includes:
- phpstan.neon

parameters:
ignoreErrors:
-
message: '#unknown class Piwik\\Plugins\\CoreHome\\EntityDuplicator\\EntityDuplicatorHelper#'
path: API.php
```

Keeping these out of the plugin's main `phpstan.neon` matters. On the maximum leg the class
exists, so the same entry would match nothing, and `reportUnmatchedIgnoredErrors` — on by
default — would fail that leg instead. Unmatched entries in the minimum-only file are still
reported on the minimum leg, so an ignore that outlives the incompatibility still surfaces.

## Git hooks (`hooks/`)

`hooks/pre-push` is the canonical copy of the PHPStan pre-push hook that plugin repositories
Expand Down
26 changes: 6 additions & 20 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,27 +194,13 @@ runs:
- name: Resolve PHP version
id: resolve-php
shell: bash
# the alias table lives in scripts/bash/resolve_php_version.sh so this action and the
# reusable PHPStan workflow cannot drift apart on it. The input arrives through the
# environment rather than an expression, so a caller's value is never expanded as shell.
env:
PHP_VERSION_INPUT: ${{ inputs.php-version }}
run: |
case "${{ inputs.php-version }}" in
matomo5_min_php)
RESOLVED_VERSION="7.2"
;;
matomo5_max_php)
RESOLVED_VERSION="8.5"
;;
matomo6_min_php)
RESOLVED_VERSION="8.1"
;;
matomo6_max_php)
RESOLVED_VERSION="8.5"
;;
'')
exit 0
;;
*)
RESOLVED_VERSION="${{ inputs.php-version }}"
;;
esac
RESOLVED_VERSION=$("${{ github.action_path }}/scripts/bash/resolve_php_version.sh" "$PHP_VERSION_INPUT")

echo "version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"

Expand Down
32 changes: 32 additions & 0 deletions artifacts/bootstrap-phpstan.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik;

use Piwik\Exception\NotYetInstalledException;

class Manifest
{
public static $files = [];
}

define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__) == '/' ? '' : dirname(__FILE__));
define('PIWIK_INCLUDE_PATH', PIWIK_DOCUMENT_ROOT);

require_once PIWIK_INCLUDE_PATH . '/core/bootstrap.php';

if (!defined('PIWIK_PRINT_ERROR_BACKTRACE')) {
define('PIWIK_PRINT_ERROR_BACKTRACE', false);
}

$environment = new \Piwik\Application\Environment(null);
try {
$environment->init();
} catch (NotYetInstalledException $e) {
}
30 changes: 30 additions & 0 deletions scripts/bash/resolve_php_version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash

# Resolves the centrally managed PHP version aliases to concrete versions, so that a caller can
# track a Matomo major's floor or ceiling without repeating the number. Anything unrecognised is
# echoed back unchanged, which is what lets callers pass a literal version such as 8.2. Empty
# input echoes empty, which callers use to mean "do not set PHP up at all".
#
# This table is shared by action.yml and .github/workflows/plugin-phpstan.yml. Keep it here
# rather than inline in either: a copy in both drifts exactly when a floor moves, which is the
# moment the value matters.

case "$1" in
matomo5_min_php)
RESOLVED_VERSION="7.2"
;;
matomo5_max_php)
RESOLVED_VERSION="8.5"
;;
matomo6_min_php)
RESOLVED_VERSION="8.1"
;;
matomo6_max_php)
RESOLVED_VERSION="8.5"
;;
*)
RESOLVED_VERSION="$1"
;;
esac

printf '%s' "$RESOLVED_VERSION"