Skip to content

Liber Tertius · Chapter 5 · Working on modules

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.

≈15 min 5 steps A project that installs and runs The GitHub CLI logged in
Source

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.

  1. Lint the title

    The check lints the header the same way commitlint does with config-conventional:

    type(scope): subject

    type is one of build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test, lower-case. scope is optional and lower-case. subject does not start with a capital and does not end with a period. The whole header is at most 100 characters.

  2. Validate the manifest

    $ grim manifest:validate src/Cart/module.json
    
      ✓ src/Cart/module.json
  3. Install the unit's JavaScript dependencies

    Only for a unit with a package.json of its own. The check builds an instance even when the unit has no tests, runs pnpm install in it and then looks for every package the unit declares in node_modules, walking up from the unit's directory the way Node does. The step prints the count, for example 6/6 dependencies of the unit resolve.; a package that did not land is named in an error. Only dependencies are checked. The instance has no lock, so versions are resolved fresh from the ranges in the package.json, where your project keeps the ones its lock already pins.

  4. Run the unit's suite

    Run the unit's tests, not the project's:

    $ grim test src/Cart/Tests

    Two 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 toBe on a decoded array compares the ordering too. Use toBeMap() for that (tests/Support/JsonColumnMaps.php).

    Second, it runs with the module enabled. See below.

  5. 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/Cart

    The 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 cart

Or, 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.php

Delete 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.