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.
Before you start
The stack is up and the suite passes, so a red test after your change is yours.
$ grim up
$ grim testSending 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.
See what you are about to change
Two facts decide where the change goes: the
typein the module's manifest, and whethergrim.locknames the module.$ grep '"type"' src/Cart/module.json "type": "module", $ grep '"cart"' grim.lock "cart": { $ git status --shortmoduleand an entry in the lock make a unit, and this recipe is for it.core-moduleis part of core, sent withgrim push --core, see When it does not work.modulelisted underlocalingrim.jsonis a local module of the project: commit the change, there is nothing to send, andgrim pushsays so. An empty status keeps your change apart from anything else you were doing.Make the change and test it
Edit the files under
src/Cart/in place. The project runs the module from there, so the browser andgrim testsee the change at once.$ grim test --filter=CartTotalsTest $ grim testCommit the change in the project when it is ready, the way you commit anything else.
grim pushcomparessrc/Cart/with the releasegrim.locknames, committed or not, so the commit changes nothing about what is sent. New files count as long as the project's.gitignoredoes not exclude them.Give it packages of its own
Optional. A package only this module imports goes into a
package.jsonnext tomodule.json, not into the root one, so only projects that carry the module install it. It must be"private": trueand carry no install scripts; the rules and an example are with the validator, which checks the file with the manifest. Runpnpm installin the project afterwards and commit the changedpnpm-lock.yamlwith the project, not with the module.Check the manifest
Optional. Do it when you touched
module.json, for instance to require another module, or the module's ownpackage.json.grim manifest:validateapplies the rules the monorepo enforces on every pull request.$ grim manifest:validate src/CartOnly
~2.5passes as a constraint inrequires.^2.5and2.5.*are refused.Read what would be sent
The dry run of
grim pushfetches 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
--patchto 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.Open the pull request
The same command without the flag commits your change on the branch
grim-push/module/cartin the monorepo, pushes it and opens the pull request withgh.$ 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/128One module travels per push. The branch name is fixed per module and the push is forced, so pushing
Cartagain rewrites the same pull request with the current state ofsrc/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.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 testUpdate 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.lockthe 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.
commitlintreads the title and nothing else. A red one names the rule in its log on a line starting with✖, such assubject may not be emptyorheader must not be longer than 100 characters. The types it accepts are the onesgrim pushalready refused on your machine, so a redcommitlintafter a push means the title was edited on GitHub. Edit it back; the check runs again.sync-dry-runleaves 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-runThe 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/57Take 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 itSpells used here
grim push
Send a change upstream
Turn the edits you made to one installed module or theme into a pull request against the monorepo it is released from.
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 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.
grim update
Board the newest train
Move core, every module and every theme onto the newest release train grim.json allows, or a single module within the train it is on.
grim module:info
Read a module's entry
Show a module's description, latest release, last push and archived status straight from GitHub, with its README on request.
grim auth:login
Log grim in to GitHub
Give grim a GitHub token, from your gh login or a personal access token, and hand the same token to Composer and Docker.
Related recipes
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.
Onboard as a collaborator
Get access to the packages, log grim in, clone the project, install what its lock records, bring the stack up and run the suite once.