Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/check-extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../resolve-check-config/check-extension.schema.json",
"jobs": {
"phpstan": true
}
}
40 changes: 38 additions & 2 deletions .github/workflows/check-extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ jobs:
outputs:
resolved: ${{ steps.resolve.outputs.resolved }}
steps:
- uses: graycoreio/github-actions-magento2/resolve-check-config@main
## resolve-check-config reads `.github/check-extension.json` from the workspace,
## so the caller's repository has to be on disk before it runs.
- uses: actions/checkout@v7

- uses: graycoreio/github-actions-magento2/resolve-check-config@phpstan
id: resolve
with:
kind: extension
Expand Down Expand Up @@ -214,6 +218,38 @@ jobs:
path: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }}

phpstan:
runs-on: ${{ matrix.os }}
needs: [compute_resolved, compute_latest_matrix]
if: ${{ fromJSON(needs.compute_resolved.outputs.resolved)['phpstan'].enabled != false }}
strategy:
matrix: ${{ fromJSON(needs.compute_latest_matrix.outputs.matrix) }}
fail-fast: ${{ inputs.fail-fast }}
steps:
- uses: actions/checkout@v7

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v${{ matrix.composer }}
coverage: none

- uses: graycoreio/github-actions-magento2/cache-magento@main
with:
composer_cache_key: ${{ inputs.composer_cache_key && format('{0} | {1}', inputs.composer_cache_key, matrix.magento) || matrix.magento }}

- name: Composer install
working-directory: ${{ inputs.path }}
run: composer install
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}

- uses: graycoreio/github-actions-magento2/phpstan@phpstan
with:
working-directory: ${{ inputs.path }}
error_format: github
composer_auth: ${{ secrets.composer_auth }}

integration_test:
runs-on: ${{ matrix.os }}
needs: compute_resolved
Expand Down Expand Up @@ -311,7 +347,7 @@ jobs:
gate:
name: Check Extension Success
runs-on: ubuntu-latest
needs: [compute_resolved, unit-test-extension, compile-extension, compute_latest_matrix, coding-standard, integration_test]
needs: [compute_resolved, unit-test-extension, compile-extension, compute_latest_matrix, coding-standard, phpstan, integration_test]
if: always()
steps:
- name: Check results
Expand Down
59 changes: 58 additions & 1 deletion .github/workflows/check-store.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,63 @@ jobs:
path: ${{ steps.setup-magento.outputs.path }}
composer_auth: ${{ secrets.composer_auth }}

phpstan:
runs-on: ${{ matrix.os }}
needs: compute_matrix
if: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['phpstan'].enabled != false }}
strategy:
matrix: ${{ fromJSON(needs.compute_matrix.outputs.resolved)['phpstan'].matrix }}

steps:
- uses: actions/checkout@v7
if: inputs.store_artifact_name == ''

- uses: actions/download-artifact@v8
if: inputs.store_artifact_name != ''
with:
name: ${{ inputs.store_artifact_name }}
path: ${{ inputs.path }}

- uses: graycoreio/github-actions-magento2/setup-magento@main
id: setup-magento
with:
php-version: ${{ matrix.php }}
tools: composer:v${{ matrix.composer }}
mode: store
working-directory: ${{ inputs.path }}
composer_auth: ${{ secrets.composer_auth }}

- uses: graycoreio/github-actions-magento2/cache-magento@main
with:
composer_cache_key: ${{ inputs.composer_cache_key }}
working-directory: ${{ steps.setup-magento.outputs.path }}
stamp: ${{ inputs.stamp }}

- name: Composer install
working-directory: ${{ steps.setup-magento.outputs.path }}
run: composer install
env:
COMPOSER_AUTH: ${{ secrets.composer_auth }}

- name: Check for custom code
id: custom-code
working-directory: ${{ steps.setup-magento.outputs.path }}
run: |
if [ -n "$(find app/code -name '*.php' -print -quit 2>/dev/null)" ]; then
echo "found=true" >> $GITHUB_OUTPUT
else
echo "found=false" >> $GITHUB_OUTPUT
fi

## PHPStan fails outright on an empty path, so a store with no app/code has nothing to analyze.
- uses: graycoreio/github-actions-magento2/phpstan@phpstan
if: steps.custom-code.outputs.found == 'true'
with:
working-directory: ${{ steps.setup-magento.outputs.path }}
path: app/code
error_format: github
composer_auth: ${{ secrets.composer_auth }}

smoke-test:
runs-on: ${{ matrix.os }}
needs: compute_matrix
Expand Down Expand Up @@ -251,7 +308,7 @@ jobs:
gate:
name: Check Store Success
runs-on: ubuntu-latest
needs: [compute_matrix, unit-test, coding-standard, smoke-test]
needs: [compute_matrix, unit-test, coding-standard, phpstan, smoke-test]
if: always()
steps:
- name: Check results
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ If the reusable workflows do not fit your needs, the individual actions are the
| [Get Magento Version](./get-magento-version/README.md) | A Github Action that computes the installed Magento version. |
| [Get Composer Version](./get-composer-version/README.md) | A Github Action that computes an installed Composer version. |
| [Coding Standard](./coding-standard/README.md) | A Github Action that runs the Magento Coding Standard. |
| [PHPStan](./phpstan/README.md) | A Github Action that runs PHPStan with the Magento PHPStan extension. |
| [Semver Compare](./semver-compare/README.md) | A Github Action that semantically compares two versions |
| [Supported Version](./supported-version/README.md) | A Github Action that computes the currently supported Github Actions Matrix for Magento 2 |
| [Setup Install](./setup-install/README.md) | A Github Action that runs `bin/magento setup:install` from the supported-version services matrix |
Expand Down
2 changes: 1 addition & 1 deletion docs/workflows/check-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The Magento matrix format outlined by the [supported versions action.](https://g

## Configuration

Each check can be toggled on/off through an optional `.github/check-extension.json` file in the repo that calls this workflow.
Each check can be toggled on/off through an optional `.github/check-extension.json` file in the repo that calls this workflow.

You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):

Expand Down
3 changes: 2 additions & 1 deletion docs/workflows/check-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ See the [check-store.yaml](../../.github/workflows/check-store.yaml)

- **Unit Tests** — runs PHPUnit against custom code in `app/code`. Skipped automatically if no test files are found.
- **Coding Standard** — runs the Magento Coding Standard against `app/code`. Uses your `phpcs.xml` (or `.phpcs.xml`, `phpcs.xml.dist`, `.phpcs.xml.dist`) if one exists, otherwise a sensible default is generated.
- **PHPStan** — Runs [PHPStan](../../phpstan/README.md) against `app/code`. Uses your [`phpstan.neon`](https://phpstan.org/config-reference) (or `phpstan.neon.dist`, `phpstan.dist.neon`) if one exists, otherwise a sensible default is generated.
- **Smoke Test** — boots your store against the supported-version service set (mysql, search, queue, cache, nginx, php-fpm) and runs the smoke probes against it.

## Configuration

Each check can be toggled on/off through an optional `.github/check-store.json` file in the repo that calls this workflow.
Each check can be toggled on/off through an optional `.github/check-store.json` file in the repo that calls this workflow.

You can learn more about this file here in the [`resolve-check-config` action.](../../resolve-check-config/README.md):

Expand Down
52 changes: 52 additions & 0 deletions phpstan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# PHPStan Action

A Github Action that runs [PHPStan](https://phpstan.org/) with the [Magento PHPStan extension](https://github.com/bitExpert/phpstan-magento).

> [!NOTE]
> PHPStan resolves classes through Composer's autoloader. Point `working-directory` at a project where `composer install` has already run, so `Magento\...` references resolve.

## Inputs

See the [action.yml](./action.yml)

## Configuration

If `working-directory` contains a [`phpstan.neon`](https://phpstan.org/config-reference), `phpstan.neon.dist`, or `phpstan.dist.neon`, that file is used as-is and `path`/`level` are ignored. Otherwise a config is generated that includes the Magento extension, analyzes `path` at `level`, and excludes `vendor/`, `generated/`, `var/`, `dev/`, and `setup/` (unless `path` is itself inside `vendor/`). The generated config lives outside the checkout, so your repository is left untouched.

Integration and API tests are excluded when `working-directory` has no `dev/tests/integration/framework` — `Magento\TestFramework` ships inside a Magento install, so analyzing an extension against its own dependencies cannot resolve it. Point `working-directory` at a Magento root and they are analyzed.

## Usage

The caller is responsible for checking out the repository, setting up PHP, and installing dependencies before calling this action.

```yml
name: PHPStan

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none

- run: composer install

- uses: graycoreio/github-actions-magento2/phpstan@v8.9.0 # x-release-please-version
with:
path: app/code # Optional, defaults to .
level: 2 # Optional, defaults to 1.
error_format: github # Optional, emits workflow annotations instead of a table.
```
165 changes: 165 additions & 0 deletions phpstan/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: "PHPStan"
author: "Graycore"
description: "A Github Action that runs PHPStan with the Magento PHPStan extension."

inputs:
working-directory:
required: false
default: "."
description: "The project root containing `vendor/`. Accepts the output of the setup-magento action."

path:
required: false
default: "."
description: "The directory containing the code to analyze, relative to `working-directory`. Only used when generating a config; ignored when a phpstan config file is found."

level:
required: false
default: "1"
description: "The PHPStan rule level to analyze at. Only used when generating a config; a phpstan config file of your own supplies its own level."

version:
required: false
default: "^2"
description: "The version of phpstan/phpstan to use."

phpstan_magento_version:
required: false
default: "*"
description: "The version of bitexpert/phpstan-magento to use."

configuration:
required: false
default: ""
description: "Path to a phpstan config file, absolute or relative to `working-directory`. If not provided, an existing phpstan.neon/phpstan.neon.dist/phpstan.dist.neon in `working-directory` is used, otherwise one is generated."

memory_limit:
required: false
default: "-1"
description: "The memory limit to run PHPStan with. Analyzing Magento needs more than PHP's default."

error_format:
required: false
default: ""
description: "The PHPStan error format (e.g. `github` to emit workflow annotations). Defaults to PHPStan's table output."

composer_auth:
required: false
default: ""
description: "Composer authentication credentials (contents of auth.json as a JSON string)"

runs:
using: composite
steps:
- name: Check if PHPStan is already installed
id: check-installed
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
if [ -f "vendor/bin/phpstan" ] && [ -d "vendor/bitexpert/phpstan-magento" ]; then
echo "installed=true" >> $GITHUB_OUTPUT
else
echo "installed=false" >> $GITHUB_OUTPUT
fi

- name: Install PHPStan
shell: bash
working-directory: ${{ inputs.working-directory }}
## --with-all-dependencies: a plain require is a partial update, which cannot move
## anything already in the lock file. bitexpert/phpstan-magento caps symfony/finder at
## ^7 while a PHP 8.4+ tree resolves ^8, so the shared dependency has to be free to
## move or Composer settles on a release too old to know about Magento\TestFramework.
run: composer require --dev --with-all-dependencies "phpstan/phpstan:${{ inputs.version }}" "bitexpert/phpstan-magento:${{ inputs.phpstan_magento_version }}"
if: steps.check-installed.outputs.installed != 'true'
env:
COMPOSER_AUTH: ${{ inputs.composer_auth }}

- name: Resolve PHPStan configuration
id: configuration
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
CONFIGURATION: ${{ inputs.configuration }}
ANALYZE_PATH: ${{ inputs.path }}
LEVEL: ${{ inputs.level }}
run: |
if [ -n "$CONFIGURATION" ]; then
echo "file=$CONFIGURATION" >> $GITHUB_OUTPUT
exit 0
fi

for CANDIDATE in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do
if [ -f "$CANDIDATE" ]; then
echo "file=$CANDIDATE" >> $GITHUB_OUTPUT
exit 0
fi
done

## Generated outside the checkout so the caller's repository is left untouched.
## Relative paths in a neon resolve against the config file, so every path is absolute.
GENERATED="$(mktemp -d)/phpstan.neon"
cat > "$GENERATED" << EOF
includes:
- ${PWD}/vendor/bitexpert/phpstan-magento/extension.neon
parameters:
level: ${LEVEL}
paths:
- ${PWD}/${ANALYZE_PATH}
EOF

## Analyzing the project root would walk into Magento itself. An extension is
## instead analyzed from where Composer installed it, so there the project's
## vendor/ must stay in scope and only the extension's own nested one is dropped.
case "$ANALYZE_PATH" in
vendor|vendor/*)
cat >> "$GENERATED" << EOF
excludePaths:
analyse:
- ${PWD}/${ANALYZE_PATH}/vendor/*
EOF
;;
*)
cat >> "$GENERATED" << EOF
excludePaths:
analyse:
- ${PWD}/vendor/*
- ${PWD}/generated/*
- ${PWD}/var/*
- ${PWD}/dev/*
- ${PWD}/setup/*
EOF
;;
esac

## Magento\TestFramework ships in magento2-base's dev/tests, so it exists only inside
## a full Magento install. Analyzing an extension against its own dependencies cannot
## resolve it, and every integration test would report class.notFound.
if [ ! -d "dev/tests/integration/framework" ]; then
cat >> "$GENERATED" << EOF
- ${PWD}/*Test/Integration/*
- ${PWD}/*Test/Api/*
EOF
fi

echo "file=$GENERATED" >> $GITHUB_OUTPUT

- name: PHPStan Analysis
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
CONFIGURATION: ${{ steps.configuration.outputs.file }}
MEMORY_LIMIT: ${{ inputs.memory_limit }}
ERROR_FORMAT: ${{ inputs.error_format }}
run: |
FLAGS=()
[ -n "$ERROR_FORMAT" ] && FLAGS+=(--error-format="$ERROR_FORMAT") || true

vendor/bin/phpstan analyse \
--configuration="$CONFIGURATION" \
--memory-limit="$MEMORY_LIMIT" \
--no-progress \
"${FLAGS[@]}"

branding:
icon: "search"
color: "green"
Loading