Skip to content

Liber Quartus · Tome II

Release trains

Every package grim installs carries one version number, and the first two parts of it are the same for all of them.

≈10 min grim 1.0.0
Source

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.

  1. Any change to a core module. Everything else is built on core, so a different core is a different train.
  2. Any change to the skeleton of grim-core, the application files around the modules. A single line counts, a comment in config/ included.
  3. A breaking change anywhere, marked as feat!: or with BREAKING 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.0

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