Every Magento module I publish needs the same CI: validate composer.json, lint the PHP, run PHPCS and PHPStan, then PHPUnit on each release I support. This repository holds that once, as reusable GitHub Actions workflows. A module’s CI becomes a short file of uses: lines, and when Magento ships a new release the supported list changes here and every module picks it up on its next run.
As of 11th October 2026, seven of my modules call it: Verbose DB Status, Media Proxy, Fetch Priority, Auto Login Admin, TinyMCE Anchors, and the example banner and footer modules used to validate deployments.
What does a module’s CI look like with it?
Four jobs. This is the whole of Media Proxy’s .github/workflows/ci.yml, minus the on: block:
jobs:
lint:
name: Linter
uses: samjuk/github-actions/.github/workflows/magento2-test-lint.yaml@master
static:
name: Static Analysis
uses: samjuk/github-actions/.github/workflows/magento2-test-static.yaml@master
needs: lint
supported-versions:
name: Supported Versions
uses: samjuk/github-actions/.github/workflows/magento-supported-versions.yaml@master
phpunit-tests-ghas:
name: PHPUnit Tests (GHAS)
uses: samjuk/github-actions/.github/workflows/magento2-test-phpunit-module-ghas.yaml@master
needs: [static, supported-versions]
with:
magento_version: ${{ matrix.version }}
magento_distribution: ${{ matrix.distribution }}
run_unit_tests: true
run_integration_tests: true
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.supported-versions.outputs.versions) }}
magento-supported-versions returns the release list as JSON, and the PHPUnit job fans out across it. By default that is Magento 2.4.9, 2.4.8-p5, 2.4.7-p10 and 2.4.6-p15, plus Mage-OS 3.5.0. A module that needs something different passes include or exclude lines (exclude takes a glob such as 2.4.9*) rather than keeping its own copy of the list.
What is in it?
Reusable workflows:
| Workflow | What it runs |
|---|---|
magento2-test-lint | composer validate, then php -l on every PHP file. PHP 8.4 by default. |
magento2-test-static | PHPCS with the Magento coding standard and PHPCompatibility, through a shared ruleset (140 character lines, docblock rules relaxed) at severity 6. PHPStan at level 5 with bitexpert/phpstan-magento. |
magento-supported-versions | The shared release list above. |
magento2-test-phpunit-module-ghas | Unit tests, setup:di:compile and integration tests for a module. |
magento2-test-phpunit-project-ghas | The same for a whole project, globbing app/code. |
magento2-test-unit-*-ghas, *-warden | The older unit-only workflows, in a GitHub services or a Warden flavour. The PHPUnit workflows replace the GHAS ones. |
Actions they build on:
magento/compute-software-requirementsmaps a Magento or Mage-OS version to its PHP, Composer, MySQL, MariaDB, OpenSearch, Redis, Valkey, Varnish and RabbitMQ versions. The data is amanifest.jsoncovering 70 Magento versions from 2.4.4 to 2.4.9, scraped from Adobe’s system requirements page with a browser console script kept next to it.warden/create-envwrites a Warden.envfrom inputs with defaults, andwarden/setup-environmentchecks out Warden at a tag and brings the environment up on the runner.
How does the PHPUnit workflow get a Magento install?
It does not install one. The job runs inside the pre-installed Magento CI testing image for that release, samjuk/magento-ci-testing-env:<version>-php<php> (or the Mage-OS equivalent), with MySQL, Redis and OpenSearch as service containers at the versions compute-software-requirements picks.
From there it requires the module from the checkout through a Composer path repository, runs Test/Unit, runs setup:di:compile, points the integration test config at the services and runs Test/Integration. Either test directory can be missing and is skipped. If tests were asked for and neither exists, the job fails rather than reporting green with nothing run, because that almost always means a wrong module_path.
How is it tested?
Against fixture modules in the repository. Each reusable workflow has an _internal-test-* workflow that calls it on changes:
- the module workflow runs against
demo-packagefor unit and for integration tests, againstdemo-package-unit-onlywith both flags on (to prove the missing integration directory is skipped), and again for an older PHPUnit, the Mage-OS image swap and a PHP version override; - the project workflow runs against
demo-project; - the supported versions workflow is called three ways (default, include and exclude, Mage-OS scoped) and an assert job compares each output with the exact JSON expected.
The demo-package-no-tests fixture, for the fail-when-nothing-ran path, was checked by hand. GitHub Actions does not allow continue-on-error on a job that calls a reusable workflow, so an expected failure would leave the self-test permanently red.
Is this the same as magento.works?
No. It answers one question: does my module still work on every release I support? The images it runs on are the Magento CI testing images, which take the Magento install out of each run. magento.works asks a different one: which service versions each Magento release actually runs on.
Cache Debounce still runs its own CI.
What is still rough?
- Fail on PHPStan. The step ends in
|| true, so findings show in the log and never block a merge. - Lint YAML or XML. Both jobs exist but exit straight away, marked TODO.
- Offer a version to pin. There are no tags, and callers use
@master, so a change here reaches every module on its next run. - Run integration tests on 2.4.4 or earlier. Those releases default to Elasticsearch 7 settings the workflow does not configure, which is why 2.4.4 is out of the default list.