Skip to content

Liber Tertius · Chapter 5 · Working on modules

Change a module and send it back

A module under src/ is a copy of a released tag, committed in the project, and its repository is written by the release process alone. So a change does not go back the way it came. It travels to the monorepo as a pull request and returns to every project as a release.

≈20 min 7 steps A project that installs and runs The GitHub CLI logged in Write access to the monorepo
Source

Before you start

The stack is up and the suite passes, so a red test after your change is yours.

$ grim up
$ grim test

Sending the change needs the GitHub CLI gh, logged in, and write access to the monorepo the modules are released from. Reading it is enough for the dry run. A project that belongs to another organisation names its own monorepo in grim.json, and then that is where the pull request goes.

"upstream": { "repo": "acme/platform", "branch": "master" }

A project whose grim.json says "upstream": { "mode": "units" } has no monorepo: its units are released from their own repositories, and the pull request goes there. The ritual is the same; what differs is in If your project uses upstream.mode: units at the end.

The ritual, in order

Six steps. The fifth is the only one that leaves your machine, and the sixth happens days later, when the release is out.

  1. See what you are about to change

    Two facts decide where the change goes: the type in the module's manifest, and whether grim.lock names the module.

    $ grep '"type"' src/Cart/module.json
      "type": "module",
    $ grep '"cart"' grim.lock
        "cart": {
    $ git status --short

    module and an entry in the lock make a unit, and this recipe is for it. core-module is part of core, sent with grim push --core, see When it does not work. module listed under local in grim.json is a local module of the project: commit the change, there is nothing to send, and grim push says so. An empty status keeps your change apart from anything else you were doing.

  2. Make the change and test it

    Edit the files under src/Cart/ in place. The project runs the module from there, so the browser and grim test see the change at once.

    $ grim test --filter=CartTotalsTest
    $ grim test

    Commit the change in the project when it is ready, the way you commit anything else. grim push compares src/Cart/ with the release grim.lock names, committed or not, so the commit changes nothing about what is sent. New files count as long as the project's .gitignore does not exclude them.

  3. Give it packages of its own

    Optional. A package only this module imports goes into a package.json next to module.json, not into the root one, so only projects that carry the module install it. It must be "private": true and carry no install scripts; the rules and an example are with the validator, which checks the file with the manifest. Run pnpm install in the project afterwards and commit the changed pnpm-lock.yaml with the project, not with the module.

  4. Check the manifest

    Optional. Do it when you touched module.json, for instance to require another module, or the module's own package.json. grim manifest:validate applies the rules the monorepo enforces on every pull request.

    $ grim manifest:validate src/Cart

    Only ~2.5 passes as a constraint in requires. ^2.5 and 2.5.* are refused.

  5. Read what would be sent

    The dry run of grim push fetches the monorepo into .grim/upstream/, finds the commit your installed release was cut from, and lays your working copy over it. Nothing is sent.

    $ grim push Cart -m "fix(cart): correct totals" --dry-run
    Pushing module Cart (2.5.0) to acme/platform master
      Fetching the monorepo (blobless, sparse on src/Cart)... OK (first clone)
      Looking for the base commit... 4f2a91c chore(release): train 2.5
    
      fix(cart): correct totals
    
     src/Cart/Models/Cart.php | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    Dry run — nothing pushed.

    Read the file list. It has to be your change and nothing else, however far behind the newest train the project is. A debug file you forgot shows up here. Add --patch to read the full diff.

    The title is yours to write, and grim suggests none. It becomes both the commit message and the pull request title, so it follows the monorepo's rules: type(scope): subject, one line, at most 100 characters, a lowercase subject with no full stop. A title that breaks them is refused before anything is fetched.

  6. Open the pull request

    The same command without the flag commits your change on the branch grim-push/module/cart in the monorepo, pushes it and opens the pull request with gh.

    $ grim push Cart -m "fix(cart): correct totals"
    …
      Pushing grim-push/module/cart... OK
      Opening the pull request... OK
    
    https://github.com/acme/platform/pull/128

    One module travels per push. The branch name is fixed per module and the push is forced, so pushing Cart again rewrites the same pull request with the current state of src/Cart/. That is how you answer a review. It also means a second, unrelated fix to the same module waits until the first is merged.

  7. Take the release when it arrives

    The pull request is reviewed and merged in the monorepo, and the module gets a new release on your train. None of that happens on your machine. Until then your commit keeps the change in the project, and the project keeps running it. When the release is out, take it with grim update.

    $ git status --short
    $ grim update cart
    $ git diff --stat
    $ grim test

    Update wants a clean tree, so your change is committed by now. It merges the new release three ways against the one you installed: your edit and the released one are the same change, so the merge is clean unless the review changed it, and what is left in the diff is the rest of the release. When review did change it, resolve the conflict markers the way the merged version reads. Commit it together with the grim.lock the update wrote. Take one module fix mid-train covers this step on its own, including what to do when a newer train is already out.

What you have now

After step five, a pull request in the monorepo with a three-line body: the module, the release it was installed at, and the base commit. In the project, .grim/upstream/ holds a partial clone of the monorepo that later pushes reuse. It is ignored by git and safe to delete.

After step six, src/Cart/ holds the new release, grim.lock names that version, and every other project gets the same fix with its next update. Your colleagues on this project have the change from the moment you push the project's commit, release or not.

Checks on your pull request

Three checks run on the pull request, and two of them can turn red on their own.

  • commitlint reads the title and nothing else. A red one names the rule in its log on a line starting with ✖, such as subject may not be empty or header must not be longer than 100 characters. The types it accepts are the ones grim push already refused on your machine, so a red commitlint after a push means the title was edited on GitHub. Edit it back; the check runs again.
  • sync-dry-run leaves a comment on the pull request with the release plan the merge would trigger. Read the table: your module has to be in it with a patch step (2.5.0 -> 2.5.1) and the reason has to be your title. A module you did not touch in the table, or a line about the train moving, means the change reached more than you meant it to.
  • GitGuardian looks for secrets in the diff. It is red when a token or a key went in with the change, and that is fixed by removing it and pushing again, then rotating the secret.

What happens after the merge

The reviewer squash-merges the pull request, and from there on nothing needs you. A mirror copies the monorepo's master to the organisation's copy, the release pipeline runs there, and grim monorepo:sync cuts the units that changed: grim-module-cart gets the tag from the dry run's table, 2.5.1, and a release with it. The train does not move for a module change; it moves only when core changes, and then everybody hears about it.

Minutes after the merge, when the pipeline stayed green:

$ grim module:info cart
$ grim update cart --dry-run

The first shows the new release on the unit, the second the version the project would take. When the pipeline went red, the tag is not there yet; the next merged pull request carries your change along with it, and the dry run stays quiet until then.

When it does not work

“Users is a core module”

The message goes on with Push it with core: grim push --core -m "fix(users): …". Core travels as one change: every edit inside the core boundary since the installed release — core modules, the skeleton, new files under core module directories — goes in one pull request to the monorepo. Commit first, read the dry run, then send it.

$ git commit -am "fix(users): redirect to the page that asked for login"
$ grim push --core --dry-run
$ grim push --core -m "fix(users): redirect to the page that asked for login"

The dry run lists what stays in the project, everything you changed outside core and its units. The branch is grim-push/core-<project>, named after name in grim.json (the dry run prints it), so pushing core again from this project rewrites the same pull request, and another project's core push never touches it. A rejected pull request means reverting the edit in the project.

Core changes are released as one unit, grim-core, as a patch of the train that is running — core and whatever else your commits touched, nothing more. Ending the title with the pin [release core v2.5.1] names that patch explicitly, which is worth doing when several changes land together. When your change adds a module to config/modules-core.php or takes one out, the title starts with the marker [core] +Name -Name and carries a pin with a new MAJOR.MINOR — that one is a train, and it is announced first. The monorepo's CONTRIBUTING.md says the rest: branch names, the coupling check, the style fixer.

“No commit on acme/platform master carries this exact module Cart.”

The release grim.lock names matches no commit in the monorepo: the tag never reached the monorepo branch, or it is older than the search reaches. Commit your change, run grim update to move the module to a newer release, and push again.

“Commit message rejected (commitlint runs on the PR too):”

Each line under it names one rule the title broke, such as subject must not be sentence-case or upper-case — start it lowercase. Allowed types are build, chore, ci, docs, feat, fix, perf, refactor, revert, style and test.

“Your gh token is missing the scope(s) …”

That is not a permissions problem in the monorepo. The token gh holds lacks repo or read:packages. Run grim auth:login, which adds them, and push again.

The push succeeded and the pull request did not open

Opening the pull request... failed is followed by a compare URL. The branch is already upstream. Open the URL in a browser and create the pull request by hand.

“No changes in module Cart against its base — nothing to push.”

Your working copy equals the installed release. Either the edit went into another module, or the file you added is excluded by the project's .gitignore.

If your project uses upstream.mode: units

The six steps hold. Three of them read differently.

Read what would be sent. Nothing is fetched. The base is the release src/Cart/ sits on, and the dry run names the unit's own repository:

$ grim push Cart -m "fix(cart): correct totals" --dry-run
Pushing module Cart (2.5.0) to grimoiry/grim-cart main as grim-push/fix-cart-correct-totals
 Models/Cart.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

  fix(cart): correct totals

Dry run — nothing pushed.

Open the pull request. The branch is named after the title, and it lives in the unit's repository. Pushing again with the same title rewrites the same pull request; that is how you answer a review. A change that spans units goes in one command and comes out as one pull request per unit, each body naming the others under Part of:, and grim prints which to merge first when one requires the other.

$ grim push Cart Orders -m "fix(cart): correct totals"
…
  Merge order: Orders, Cart
    Cart requires orders

  Orders: pushing grim-push/fix-cart-correct-totals... OK, opening the pull request... OK
  Cart: pushing grim-push/fix-cart-correct-totals... OK, opening the pull request... OK

https://github.com/grimoiry/grim-orders/pull/12
https://github.com/grimoiry/grim-cart/pull/57

Take the release when it arrives. The unit's own check runs on the pull request, the owner merges, and the merge to main tags the next patch by itself. There is no sync and no train to wait for; grim module:info cart shows the tag minutes after the merge.

Core still goes to the monorepo, with grim push --core as above.

Questions

Can I send a theme the same way?

Yes. Name it with --theme instead of the module argument, as grim push --theme frontend/shop-front -m "…". Everything else in this recipe holds, with resources/themes/frontend/shop-front in place of src/Cart.

Can I keep an edit in the project without sending it?

Yes, for a module or a theme alike. Stop after step two: the commit in the project is the whole record, and a colleague gets it with git pull. Every later grim update merges the new release three ways into your edit and leaves conflict markers only where the release changed the same lines. grim build builds the image from the working tree, so the edit reaches a server with the next build and grim deploy. When the edit turns out to be worth having in every project, send it then; grim push diffs against the release grim.lock names.

Can I send a module I made with grim make:module?

A local module, made with --no-repo, has nowhere to go: it lives in the project, and a commit there is the whole change. A unit of your own has a repository and no copy in the monorepo, so no base commit exists there. With upstream.mode: units grim push opens the pull request in its repository; otherwise release it from a clone outside the project, as Create a module shows.

Next recipe

Read the unit check on your pull request

What the single `unit` check on a grim-* pull request does, how to read its annotations, and how to reproduce every one of its steps on your own machine before you push again.

Read it

Spells used here