Read the unit check on your pull request
A pull request into a module or theme repository gets one check. It builds a throwaway project around your branch, runs that unit's own suite against MySQL, and puts every failure on the line that caused it. Everything it does, you can do locally.
What you are looking at
Open a pull request in grimoiry/grim-cart, or any other grim-* module or theme, and one check appears: unit / check. One, not eight — a check list you can read at a glance is the whole point of it.
It is green only when all of this held:
| the pull request title is a Conventional Commits header | fix(cart): round totals to two places |
module.json / theme.json matches the manifest schema |
what grim manifest:validate checks |
| a real project built around your branch installed cleanly | grim-core on the current train, your unit, and everything in its requires |
| PHPStan found nothing new in the unit's files | against grim-core's baseline, see below |
| the unit's own suite passed against MySQL 8 | php artisan test, not SQLite |
| the suite actually ran | see Nothing ran below |
A failure is annotated on the file and line that caused it, spelled the way you see it in your repository — Tests/Feature/CartTest.php, not instance/src/Cart/Tests/Feature/CartTest.php. Click the annotation in the Files changed tab and you are on the line.
There are no warning annotations. If you ever see one, that is a bug in the check, not something to live with.
The ritual, in order
Five steps, in the order the check runs them. The check builds a project around your branch; you already have a project, so you can run every one of them in it — this is the loop from Change a module and send it back, with the two steps people skip.
Lint the title
The check lints the header the same way
commitlintdoes withconfig-conventional:type(scope): subjecttypeis one ofbuild,chore,ci,docs,feat,fix,perf,refactor,revert,style,test, lower-case.scopeis optional and lower-case.subjectdoes not start with a capital and does not end with a period. The whole header is at most 100 characters.Validate the manifest
$ grim manifest:validate src/Cart/module.json ✓ src/Cart/module.jsonInstall the unit's JavaScript dependencies
Only for a unit with a
package.jsonof its own. The check builds an instance even when the unit has no tests, runspnpm installin it and then looks for every package the unit declares innode_modules, walking up from the unit's directory the way Node does. The step prints the count, for example6/6 dependencies of the unit resolve.; a package that did not land is named in an error. Onlydependenciesare checked. The instance has no lock, so versions are resolved fresh from the ranges in thepackage.json, where your project keeps the ones its lock already pins.Run the unit's suite
Run the unit's tests, not the project's:
$ grim test src/Cart/TestsTwo things the check does that your project may not. First, it runs against MySQL. A suite that is green on SQLite and red in the check is usually a JSON column: MySQL 8 stores a native JSON column and hands the keys back in its own order, so
toBeon a decoded array compares the ordering too. UsetoBeMap()for that (tests/Support/JsonColumnMaps.php).Second, it runs with the module enabled. See below.
Analyse the unit with PHPStan
The check analyses only the unit, against grim-core's baseline, with unmatched baseline entries ignored, because analysing one module leaves most of the baseline unmatched and PHPStan reports that as an error of its own. On core 1.3.25 or later your project ships the same settings as
phpstan-instance.neon. Run it in the app container, where the application boots:$ grim composer -- analyse -- -c phpstan-instance.neon --no-progress src/CartThe whole loop, and how to tell your finding from one the project inherits, is in Analyse a project with PHPStan.
Nothing ran
The check fails with:
138 tests, 138 of them skipped — nothing ran.That is ModuleAwareTesting. A module whose provider is not listed in storage/config/modules-enabled.php has its whole suite skipped — green, silently, with no warning. The check refuses to call that a pass, because it once let five failures through to tenant CI that had never failed anywhere before.
grim install writes that file from the resolved closure, so in the check it means the install did not do its job. In your own project it usually means the module is installed but not enabled:
$ grim require cartOr, if you are reproducing a tenant and only want the file for a moment:
$ printf '%s\n' '<?php' '' 'return [' ' Modules\Cart\CartProvider::class,' '];' \
> storage/config/modules-enabled.phpDelete it when you are done. Tests: N skipped is the answer "it did not run" — never read it as "it passed".
When it does not work
The check is green in twenty seconds and says it ran nothing. The unit carries no Tests/Unit and no Tests/Feature, so there was no instance to build. Your title and your manifest were still checked. Add a test and the next pull request gets the whole thing.
Nothing ran on a push to main. Expected, for now: while release-mode in grim-core says monorepo the sync is what publishes the unit, and it already tested this tree on its pull request. The push path wakes up at the freeze.
The check does not appear at all. The unit has no .github/workflows/unit.yml yet. It is not added in the unit's repository — the file lives in the monorepo under the unit and arrives through the split. Ask for the rollout rather than committing it: a hand commit on a grim-* default branch makes the next sync fail.
grim install did not place the unit under src/. The unit's directory name and its repository name disagree beyond the usual kebab-to-Pascal rule. grim-oauth is the known one — src/OAuth, not src/Oauth — and the check looks case-insensitively for exactly that reason. A new mismatch is a naming bug worth reporting.
The whole check is red before any test ran. Read from the top: the title, the manifest and the install all fail loudly and early, and each says which one it was.
It takes longer than ten minutes. The install is the expensive half, and it rebuilds the world on every run until the per-train base image lands (B4). Until then, a pull request touching several units pays it once per unit.
Next recipe
Create a module
Scaffold a module, give it a table, check that it is enabled, write its first test, validate the manifest and commit it in the project — or, for a module other projects will install, publish a first version and add it to grim.json.
Read itSpells used here
grim test
Run the test suite
Run the project's tests in the app container against a separate test database that grim creates and migrates for you.
grim install
Assemble the whole project
Turn grim.json into a working project by fetching core, every module and theme, writing grim.lock, building the image and starting the stack.
grim manifest:validate
Check a manifest
Validate module.json, theme.json and core.json against the manifest schema on your machine, before CI does it for you.
Related recipes
Change a module and send it back
Check what kind of module you are in, make, test and commit the change, read the dry run, open the pull request, and take the release when it arrives.
Take one module fix mid-train
Read the one-row plan, move a single module to its newest release on the train the project is locked to, run its migrations, test, commit the module with the lock and land it on the other machines.
Analyse a project with PHPStan
Run PHPStan over a project the way the unit check runs it over one module, inside the app container, against grim-core's baseline, and tell a finding of your own from one the project inherits.