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
| Path | With a pin in place |
|---|---|
cms9:auto-update (nightly) | skips the module, writes the reason into writable/logs/auto-update.log |
cms9:module-update --all | lists the module as pinned at vX — skipped, exit code unaffected |
cms9:module-update <module> | refuses, exit code 1 |
| Update button on the store shelf | disabled, 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-pin | updates, 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()returnspinned,pin_reasonandpin_blockedalongsidelatest— 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 flaggedsecurity: trueadds 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, orcliwhen pinned from the command line;at— unix time of the pin (or of the last--ignore-pinmove);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:
- template changes → theme overrides for module templates;
- behaviour changes → an add-on module reacting to events and render points;
- edits already made to an installed module → local changes in a module
explains how
cms9:module-driftfinds them and how a patch carries them forward.
Move the change to one of those, then release the pin.