how local and pipeline checks fit together
When PHPStan is green locally but red in the CI pipeline, it comes down to a configuration mismatch. The problem is solvable: align pre-commit hooks, PhpStorm quality gates and the CI pipeline so that what runs locally checks exactly what the pipeline checks, no push that fails right out of the gate.
Table of Contents
- 1. The CI parity problem: why local doesn't equal pipeline
- 2. Pre-commit hooks: catching errors before the push
- 3. Bundling local quality gates in PhpStorm
- 4. GitHub Actions: PHP quality checks in the pipeline
- 5. GitLab CI: identical checks with Docker images
- 6. Running PHPUnit identically locally and in CI
- 7. Caching in CI: Composer and the PHPStan baseline
- 8. Seeing CI feedback in PhpStorm
- 9. Comparison: workflow with and without local CI parity
- 10. Summary
- 11. FAQ
1. The CI parity problem: why local doesn't equal pipeline
The most common reason for the gap between local and pipeline checks is environment differences: the local PHP has a different version than the CI container, Composer packages have a different lock file state locally, or PHPStan runs with different configuration paths or a different level. In Docker-based setups this problem is almost fully solvable, because you can use the same container environment locally as in the pipeline. Without Docker, the gap is structurally harder to close.
A second problem: what shows up as an inspection in PhpStorm and what the CI pipeline checks are often not the same thing. PhpStorm inspections are incremental and IDE-specific. PHPStan and PHPCS in the pipeline run with project-specific configuration files that the IDE doesn't automatically load. The goal, therefore, isn't to replace PhpStorm inspections with CI, but to align both levels and insert pre-commit hooks as a third level in between.
The three-level model for PHP quality assurance: (1) PhpStorm inspections while typing, fast, incremental, incomplete. (2) Pre-commit hooks at commit time, complete for the changed files, blocking. (3) The CI pipeline at push time, complete for the entire project, the authoritative instance. Each level has its own job; none replaces another.
2. Pre-commit hooks: catching errors before the push
Git hooks in the .git/hooks/ directory are the classic tool for pre-commit checks, but they have a drawback: they are not committed to the repository and must be set up manually by every developer. The pre-commit framework (Python based) or a simple composer scripts solution are better alternatives for PHP projects.
A lean solution without an external framework: a Composer script pre-commit that runs PHPCS on the changed files and PHPStan on the project, plus a prepare-commit-msg hook that calls this script. The script is committed to the repository, and the hook reference is set up automatically via composer install. In composer.json, under scripts.post-install-cmd, the hook link is set.
// composer.json - Pre-Commit Integration
{
"scripts": {
// Quality gate script
"check": [
"@phpcs",
"@phpstan",
"@phpunit-fast"
],
"phpcs": "vendor/bin/phpcs --standard=phpcs.xml",
"phpcbf": "vendor/bin/phpcbf --standard=phpcs.xml",
"phpstan": "vendor/bin/phpstan analyse --no-progress --memory-limit=1G",
"phpunit-fast": "vendor/bin/phpunit --testsuite=unit --no-coverage",
// Hook setup after composer install/update
"post-install-cmd": [
"test -d .git && cp scripts/pre-commit.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit || true"
],
"post-update-cmd": [
"@post-install-cmd"
]
},
"scripts-descriptions": {
"check": "Runs all quality gate checks (PHPCS + PHPStan + PHPUnit)",
"phpcs": "PHP CodeSniffer: check code style",
"phpcbf": "PHP Code Beautifier: automatically fix code style",
"phpstan": "PHPStan static analysis",
"phpunit-fast": "PHPUnit unit tests (without coverage)"
}
}
PhpStorm can run Composer scripts directly from the Composer tool window. The tool window opens via View → Tool Windows → Composer and lists all defined scripts. Clicking check runs all quality gate checks. For even faster feedback, you can set up the check script as a run configuration and assign it a shortcut.
3. Bundling local quality gates in PhpStorm
A local quality gate is a defined checkpoint before the commit that ensures code meets certain quality requirements. In PhpStorm, a quality gate can be represented as a compound run configuration: a configuration that runs PHPCS, PHPStan and PHPUnit one after another and stops on failure. The compound stops at the first failed step, so you immediately know which tool found a problem.
For everyday work, a tiered strategy is recommended: a fast quality gate (PHPCS + PHPStan on the current file, under 5 seconds) on save or on a shortcut, and a full quality gate (everything on the whole project) before the commit. The fast gate gives immediate feedback, the full gate gives confidence before the push.
4. GitHub Actions: PHP quality checks in the pipeline
GitHub Actions workflows for PHP projects can be edited directly in PhpStorm, the IDE recognizes .github/workflows/*.yml files and offers autocompletion for actions syntax. For maximum parity with the local Docker setup, it's recommended to use the same PHP container image in the pipeline as locally.
A minimal but complete GitHub Actions workflow for PHP quality checks: pin the PHP version, use a Composer cache, run composer install, then run PHPCS, PHPStan and PHPUnit sequentially. Each step has a clear name and the result shows up in the GitHub UI as an individual check status. If PHPStan fails, you know immediately which tool caught the problem.
# .github/workflows/quality.yml
name: PHP Quality Checks
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
container:
# Same container as locally (Mark Shust image)
image: markoshust/magento-php:8.4-fpm-0
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Composer packages
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: PHP_CodeSniffer
run: composer phpcs -- --report=checkstyle | cs2pr
- name: PHPStan
run: composer phpstan
- name: PHPUnit
run: composer phpunit-fast
env:
MAGENTO_UNIT_TEST: "1"
The tool cs2pr (Code Style to Pull Request) converts PHPCS output into GitHub annotations that appear directly as comments in the pull request. That way, during code review, the developer sees exactly which line has a PHPCS violation without having to read the raw output. Installation: composer require --dev staabm/annotate-pull-request-from-checkstyle.
5. GitLab CI: identical checks with Docker images
GitLab CI works analogously to GitHub Actions but uses a different YAML syntax and offers native Docker registry integration. The advantage with GitLab: you can host your own Docker images with pre-installed PHP dependencies in your own registry, which significantly reduces pipeline startup times.
In .gitlab-ci.yml you define one pipeline job per quality tool. With GitLab CI artifacts, you can attach PHPUnit coverage reports and PHPStan output as downloadable artifacts of the pipeline. This allows follow-up investigation when a pipeline run fails, without rerunning the check locally. PhpStorm Ultimate has native GitLab CI integration that shows pipeline status directly in the IDE.
6. Running PHPUnit identically locally and in CI
PHPUnit configuration must be identical between local execution and CI. The most important source of divergence: environment variables. Magento unit tests need certain constants and paths that are available locally in the container but must be set in CI. With a phpunit.xml.dist in the repository and a local phpunit.xml (which is in .gitignore), you can cover both scenarios.
PhpStorm reads phpunit.xml.dist automatically for run configurations. The bootstrap file loads Magento-specific constants and the autoloader. In the CI pipeline, the PHPUnit call references the same configuration file, which ensures identical behavior. Code coverage should run locally on demand and in CI on every PR build, but never in pre-commit hooks, since they are too slow for that.
<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml.dist - Repository configuration for CI and local execution -->
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="src/dev/tests/unit/framework/bootstrap.php"
colors="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
>
<testsuites>
<testsuite name="unit">
<directory>src/app/code/Mironsoft/*/Test/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>src/app/code/Mironsoft/*/Test/Integration</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src/app/code/Mironsoft</directory>
</include>
<exclude>
<directory>src/app/code/Mironsoft/*/Test</directory>
</exclude>
</source>
<php>
<!-- Environment variables for Magento tests -->
<env name="MAGENTO_ROOT" value="src"/>
<env name="TESTS_CLEANUP" value="enabled"/>
</php>
</phpunit>
7. Caching in CI: Composer and the PHPStan baseline
Caching is the single most important lever for fast CI pipelines. Without caching, composer install runs from scratch on every commit and takes several minutes on large PHP projects. With a Composer cache based on the hash of composer.lock, install becomes a matter of unpacking already-cached packages, seconds instead of minutes.
PHPStan baseline files need to be available as an artifact between builds in CI. If the baseline is generated locally and committed to the repository, that's not a problem, it gets checked out along with the code. Important: don't add the baseline to .gitignore. PHPStan's internal caches (/tmp/phpstan/) can also be cached in CI, but they bring less benefit than the Composer cache.
8. Seeing CI feedback in PhpStorm
PhpStorm Ultimate has a GitHub integration that shows pull request status, check results and code review comments directly in the IDE. The GitHub plugin (built into PhpStorm Ultimate) shows all open PRs with their CI status in the Pull Requests tool window. If a CI job fails, it shows up immediately in PhpStorm, without needing to open the browser.
An analogous plugin exists for GitLab projects. In both cases it's worth configuring the CI notifications so that only failures, and not every successful build, trigger an IDE notification. Too many notifications lead to habituation and get ignored. Better: notifications only for red builds, and green builds as a passive status icon in the IDE footer.
9. Comparison: workflow with and without local CI parity
The difference in everyday workflow between a setup without and with local CI parity is substantial, not just in the number of failed pipeline runs, but also in the time developers spend debugging and switching context.
| Aspect | Without parity | With local parity | Time saved |
|---|---|---|---|
| Error detection | After push in CI (minutes) | At commit time (seconds) | 5 to 15 min per error |
| Context switching | IDE to GitHub to IDE | Within the IDE | Cognitive load significantly reduced |
| Pipeline costs | Many unnecessary runs | Only valid code pushed | CI minutes reduced |
| Code review quality | Style fixes in reviews | Reviews focused on logic only | Reviews more focused and shorter |
| Onboarding | Every developer their own practice | Unified quality gates | New developers productive faster |
The investment in local CI parity is primarily a configuration task: Docker setup, identical PHP versions, shared configuration files (phpstan.neon, phpcs.xml, phpunit.xml.dist) and pre-commit hooks. Once set up, the system runs without further effort and automatically keeps the quality level high.
10. Summary
The core problem between local and pipeline checks is environment inconsistency: different PHP versions, different configuration files, missing environment variables. The solution is consistent parity: Docker setup locally and in CI, shared configuration files in the repository, pre-commit hooks that run the same tools with the same configurations as the pipeline.
In this setup, PhpStorm plays the role of the early warning system: a fast, incremental feedback loop while typing. Pre-commit hooks are the safety net before the commit. The CI pipeline is the authoritative checking instance and only ever sees code that has already been checked locally. Composer scripts as a shared abstraction layer ensure that all three levels use the same commands.
PhpStorm & CI/CD, the essentials at a glance
Three-level model
PhpStorm (while typing) to pre-commit hook (at commit time) to CI pipeline (at push time). Each level has its own job, none replaces another.
Composer scripts
Shared abstraction layer: composer check calls PHPCS, PHPStan, PHPUnit. Identical locally, in the pre-commit hook and in CI.
GitHub Actions
Pin the PHP version, Composer cache, PHPCS with cs2pr for PR annotations, PHPStan, PHPUnit sequentially. Same Docker container as locally.
CI feedback in the IDE
PhpStorm Ultimate: GitHub/GitLab plugin shows PR status directly in the IDE. Notifications only on failures. Green builds as a passive status indicator.
Mironsoft
CI/CD integration, quality gates and DevOps for PHP teams
Quality gates that run identically locally and in CI?
We set up pre-commit hooks, PhpStorm quality gates and CI pipelines so that what runs locally checks exactly what the pipeline checks, no push that fails right out of the gate, no waiting on CI feedback for fixable code.
Pre-commit setup
Set up Composer scripts, Git hooks and PhpStorm run configurations for local quality gates
CI pipeline
GitHub Actions or GitLab CI with caching, PR annotations and the same container as locally
PHPUnit CI
Harmonize PHPUnit configuration for local and CI execution, coverage reports as CI artifacts
11. FAQ: PhpStorm and CI/CD
1PHPStan green locally, red in CI, why?
2Setting up pre-commit hooks for PHP?
check. Copy a bash hook to .git/hooks/pre-commit. Set it up automatically via post-install-cmd.3What is cs2pr?
4Coverage locally or in CI?
5Show CI status in PhpStorm?
6Advantage of Composer scripts?
composer phpstan runs identically locally, in the hook and in CI. No duplicate configuration, no divergence.7Caching Composer in GitHub Actions?
actions/cache@v4 with path: vendor and key: ...-${{ hashFiles('composer.lock') }}. Cache hit: seconds instead of minutes for install.8Keep pre-commit hooks from becoming too slow?
9Which configuration files into the repo?
10Sync run configurations with CI?
composer check, CI job too. Both are automatically in sync, same entry point.