Release trains
Every package grim installs carries one version number, and the first two parts of it are the same for all of them.
A project is grim-core plus a few dozen modules and themes, each in its own repository. If each of them kept its own version line, every project would face the same question on every update: does this cart work with that core? Trains take the question away. Packages released together share a number, and a shared number means they were built and tested together.
One number, three parts
Core, modules and themes all use MAJOR.MINOR.PATCH. The first two parts are the train: 2.5 is a set of packages known to work together. The last part belongs to each package alone.
| Part | Means |
|---|---|
| MAJOR | A generation of the platform, such as a PHP or Laravel upgrade. The only part a person decides. |
| MINOR | The train. When it moves, every package is released at X.Y.0, including the ones nobody touched. |
| PATCH | One package, on its own. A fix or a compatible feature in cart releases cart 2.5.3 and nothing else. |
What moves a train
Nobody types a version number into a package. The release reads the conventional commit titles merged since the last one and decides. Three kinds of change start a new train.
- Any change to a core module. Everything else is built on core, so a different core is a different train.
- Any change to the skeleton of grim-core, the application files around the modules. A single line counts, a comment in
config/included. - A breaking change anywhere, marked as
feat!:or withBREAKING CHANGE:in the commit.
Everything else, fix: and plain feat: alike, is a patch of the package it touched. That is a deliberate departure from strict SemVer. If every feature moved the train, there would be a new train every day and grim update would never be quiet.
This is why the dependencies of a module are never written by hand. The requires in its module.json is generated from what its code imports, and every entry reads ~X.Y, the train it was released on.
Only forward, one train at a time
There is one supported train, the newest. A fix is released as a patch on it, and a project on an older train gets that fix by moving to the newest one. There are no release branches, no backports and no long-term trains.
That is also why grim update refuses to move a single module once a newer train exists. A cart from 2.5 on a core from 2.4 is exactly the pair nobody tested. Inside the train a project is on, one module can take a patch alone, and the recipe Take one module fix mid-train is that move.
What a constraint in grim.json allows
grim create-project writes the train of the day into a new grim.json, for core, every module and every theme. The constraint on core decides which trains grim update may move to. Modules and themes then follow core onto that train, whatever their own constraints say.
{
"core": "~2.4",
"requires": { "cart": "~2.4", "payments": "~2.4" },
"themes": { "admin": "float:~2.4" }
}core |
Follows |
|---|---|
"~2.4" |
Train 2.4 and every later train of major 2. Composer reads a two-part tilde as >=2.4 <3.0.0, so this is not a pin. |
"2.4.*" |
Exactly one train. Patches still arrive. When 2.5 ships, grim update reports that the project is up to date. |
"^2" |
Every train of major 2. |
grim require follows the same rule from the other side. It writes the constraint of the project's train for the new module and resolves it, and everything it depends on, inside that train.
The lock is where the project is
grim.json says what the project may follow. grim.lock records where it is: the train, the exact core version and commit, and the version and commit of every module and theme. Read the lock, not grim.json, when you need to know what runs.
The two commands that read it do opposite things. grim install reproduces the lock, so a fresh clone lands the versions your colleagues committed even when newer ones exist. grim update moves the project and writes the lock again, whole. The recipe Move a project to a new train is the second one done with care.
Tags, and the one thing not on the train
Package tags are bare numbers: 2.5.0, never v2.5.0. The CLI is the exception. grim itself is not part of any train, has its own version line, and tags its releases with a v.
grimoiry/grim-core 2.5.0
grimoiry/grim-cart 2.5.3
grimoiry/grim-theme-admin-float 2.5.0
grimoiry/grim-cli v1.4.0A new grim does not move a project's train, and a new train does not need a new grim. They meet in one place, the infrastructure files, which come from the templates of the CLI and are renewed with grim upgrade.
Put it to use
Move a project to a new train
Read the train plan, decide what core stopped shipping, update, renew the infrastructure files, and commit a lock you have tested.
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 lock and land it on the other machines.
Spells that do it
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 require
Summon a module or theme
Add a module or theme to grim.json, install it with everything it depends on, and rewrite grim.lock, all inside the train the project is on.
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.