Skip to main content

Holding one module on the version it runs

Module auto-update used to be all-or-nothing. writable/auto_update_modules.on swept every installed store module whose catalog version was newer, and the only thing that could stop one of them was the local-changes detector — that is, an accident, not a decision. A site that had integrated one module, or that had simply not tested its new release yet, could keep it still only by switching automatic updates off for everything.

A pin is the per-module answer: the module stays on the version it runs, and every other module keeps updating.

A pin is a temporary measure, not a way to live. It holds a module while you verify a release, while an integration is being re-tested, or while a client sign-off is pending — and then it is released. A module frozen for months is a module that will eventually be updated across several releases at once, which is the risky update, not the safe one. The panel and --list always show both the pinned version and the version the catalog offers, so a pin can hold an update but never hide one.

What a pin does​

PathWith a pin in place
cms9:auto-update (nightly)skips the module, writes the reason into writable/logs/auto-update.log
cms9:module-update --alllists the module as pinned at vX — skipped, exit code unaffected
cms9:module-update <module>refuses, exit code 1
Update button on the store shelfdisabled, the card explains why
The module's OWN update button (a module that self-updates)disabled, the banner says pinned vX, vY available
cms9:module-update <module> --ignore-pinupdates, and moves the pin to the new version

Every one of these goes through the same decision in App\Libraries\UpdateServer\ModuleInstaller::install(), so a new call site inherits the behaviour instead of having to re-implement it. The two commands additionally short-circuit before any network work, so a batch run can report a pinned module without downloading its package.

Modules that update themselves​

A few modules carry their own update client — Libraries/ModuleUpdater.php inside the module (Notify, Pixozip, CloudStorage). They talk to the update server directly and never pass through ModuleInstaller, so they ask the pin question themselves, through the same App\Libraries\UpdateServer\ModulePins::blockedFor():

  • check() returns pinned, pin_reason and pin_blocked alongside latest — computed outside the module's daily check cache, so a pin set five minutes ago grays the button out now, not tomorrow;
  • update() refuses before anything is downloaded, backed up or written, with the same sentence the CLI prints (error: pinned);
  • the module's own banner shows pinned vX, vY available and its Update button is disabled; a release flagged security: true adds the red line and still does not lift the pin.

The lookup is wrapped in class_exists() on purpose: these modules are shipped as their own packages and install on cores older than the pin feature, where ModulePins does not exist. A pin cannot exist there either, so "nothing blocks" is the right answer — not a fatal.

If you write a new module with its own updater, copy that pair of calls. A self-updater that skips them is a hole in every pin on the site.

Proving a pin without moving a whole site​

cms9:auto-update takes two flags for exactly this:

php spark cms9:auto-update --only=my_module # only this module; the CORE is left alone
php spark cms9:auto-update --only=my_module --dry-run # decide and log, write nothing

--dry-run logs would update 1.0.1 → 1.0.2 — nothing written (and the usual pinned … skipped line for a pinned module) with a [dry-run] marker in writable/logs/auto-update.log. --only is what makes the check affordable on a shared stand: without it, proving that one module is held means letting every other module on that machine update. Both option shapes work: --only=x and --only x.

Command line​

php spark cms9:module-pin my_module --reason="waiting on integration sign-off"
php spark cms9:module-pin my_module --version=1.0.3 # pin to a version other than the installed one
php spark cms9:module-unpin my_module
php spark cms9:module-pin --list
php spark cms9:module-pin --list --json

Both option shapes are accepted — --reason=text and --reason text.

Exit codes: 0 on success, 1 when the module identif is malformed, when the module is not installed (nothing to pin it to), when it ships inside the core zip (see below), or — for cms9:module-unpin — when it was not pinned at all.

Admin panel​

On the store shelf (Modules → Store) an installed module gets:

  • Pin to this version — asks for a reason, then freezes the module;
  • a badge pinned: vX next to available: vY when the catalog is ahead;
  • Unpin — releases the pin;
  • a disabled Update button while the pin holds.

Both actions are written to the admin journal as module.pin / module.unpin with the reason, so the pin has an author and a date, not just a file entry.

Where the state lives​

writable/store_pins.json, next to store_versions.json — deliberately outside the module directory and outside its components row, so a pin survives an uninstall/reinstall of the module. It is equally deliberately not the same file as writable/module_local_changes.json: that one records what the local-changes detector happened to stop, this one records what a human chose.

{
"my_module": {
"version": "1.0.3",
"by": "owner@example.com",
"at": 1790220000,
"reason": "waiting on integration sign-off",
"security": {
"to": "1.0.5",
"since": 1790300000,
"dismissed": ""
}
}
}
  • version — the version the module is frozen on;
  • by — the admin e-mail, or cli when pinned from the command line;
  • at — unix time of the pin (or of the last --ignore-pin move);
  • reason — free text, shown in the panel and in --list;
  • security — present only while a security release is waiting; see below.

The file is written atomically (temp file + rename) and removed when the last pin is released, so an empty store_pins.json never lingers.

--ignore-pin moves the pin, it does not drop it​

php spark cms9:module-update my_module --ignore-pin installs the new version and then rewrites the pin onto it, keeping by and reason. The owner said "update this one anyway", not "stop freezing this module" — dropping the pin there would quietly re-enrol the module into the nightly sweep, which is the opposite of what a pin is for. To actually release it, run cms9:module-unpin.

Security releases: the pin still holds​

A catalog entry may carry an optional boolean security field in the module manifest:

{ "module": "my_module", "version": "1.0.5", "security": true }

update.php list/check pass the field through as it stands — nothing else on the update server interprets it.

A security release does not lift a pin automatically. A silent update is exactly what a pin exists to prevent, so instead:

  • a red banner appears at the top of the admin panel naming the module and the version that is waiting;
  • the banner stays until an admin dismisses it, and dismissing it does not update anything;
  • the store card marks the module the same way.

The decision — update now, or keep holding — stays with a human. Ordinary (non-security) releases behave identically, minus the red banner.

Boundary: modules that ship inside the core zip​

Every module that travels inside the core zip is versioned with the core: the core updater lays its files down as part of a build, and its version is the core's build number. That is Config\ModulePolicy system and standard (for example notify, gallery) — the two classes the packager never leaves out — plus any other module the installed build's own file list carries (a paid module that has no store package yet). They are pinned together with the core, by holding the core build — not one by one. cms9:module-pin refuses them, and so does the pin button in the admin (the card simply has none):

notify: Модуль оновлюється разом з ядром, зафіксувати його версію не можна.

A pin record that an older version left on such a module is not a pin: the updaters and the card ignore it, and it is removed with a line in writable/logs/auto-update.log.

When a pin is the wrong tool​

A pin buys time; it does not replace a supported extension point. If you are pinning because a new release would overwrite your edits, the edits are the problem:

Move the change to one of those, then release the pin.