Skip to content

Cells and goldens ​

What a cell is ​

A cell is a directory under the cell root (default ~/qoliber-furnace, override with FURNACE_CELLS) holding a complete Magento or Mage-OS installation served by MageBox. MageBox runs only the services around it in Docker (MySQL, MariaDB, OpenSearch, Redis, RabbitMQ, Mailpit); the cell itself is plain files on the host, served by nginx and the host's PHP-FPM for its PHP version.

bash
furnace cells            # what exists
furnace cells --urls     # plus the URLs derived for each store view
furnace catalog          # platform/PHP combinations, cell coverage, installed themes and versions

The fleet today:

CellPlatformPHPDatabase
m246Magento 2.4.6-p148.2MySQL 8.0
m247Magento 2.4.7-p98.2MariaDB 10.6
m247php81Magento 2.4.7-p98.1MySQL 8.0
m248Magento 2.4.8-p48.4MySQL 8.0
m249Magento 2.4.98.4MariaDB 10.6
m249nomsiMagento 2.4.9, without Multi-Source Inventory8.4MySQL 8.0
m248php83hyva14Magento 2.4.8-p5, Hyvä 1.48.3MySQL 8.0
mageos1Mage-OS 1.3.18.3MySQL 8.0
mageos2Mage-OS 2.3.08.3MySQL 8.0
mageos3Mage-OS 3.4.08.4MySQL 8.0
mageos3php85Mage-OS 3.4.08.5MySQL 8.0

Every cell has a default (Luma) and a hyva store view. m249nomsi is the one cell built with --without-msi: the 76 inventory packages are replaced in its root composer.json, so it advertises no msi capability and is the cell that proves an extension runs on the legacy stock. Everything furnace knows about a cell is read from the cell:

FactRead from
name, PHP version, hostname.magebox.yaml
database host, port, name, credentialsapp/etc/env.php
platform and version, theme package versionscomposer.lock
store views, their themes and inheritance, sample category and product URLsthe cell's own database
capabilities (cron, graphql, hyva, multistore, opensearch, rest, sample-data, …)installed modules and configuration

There is no manifest to keep in sync. A useful cell has Luma sample data, store codes in URLs, a dedicated integration database named <cell>_integration_tests, developer mode and an empty app/code.

Page cache

Every cell runs with Magento's full page cache enabled, in its built-in form. Varnish is not in the request path, so Varnish-specific behaviour (VCL purging, ESI, the tag header size limit) is not exercised. See status.

Provisioning a cell ​

bash
scripts/provision-cell.sh <name> <magento|mageos> <version> <php> [db-port]

scripts/provision-cell.sh m247php81 magento 2.4.7-p9 8.1
scripts/provision-cell.sh mageos3    mageos  3.4.0   8.4

The script encodes every trap that produced a cell which looked fine and was not: pinning config.platform.php so Composer resolves against the cell's PHP rather than the host's; the esconfig.xsd patch for Magento 2.4.7 and older under libxml 2.12; a narrower exclude-from-classmap so PHPUnit 10 can autoload its own event classes; two-factor authentication disabled on both the cell and the integration install config; the right PHPUnit major for the platform; a per-cell OpenSearch index prefix for the integration framework. It refuses to overwrite an existing cell. Read docs/PROVISIONING.md before building a fleet on a new host.

A cell without Multi-Source Inventory ​

bash
scripts/provision-cell.sh m249nomsi magento 2.4.9 8.4 --without-msi

--without-msi builds the cell the way merchants who drop MSI do: every inventory package (the metapackage, its composer installer and the module-inventory-* modules, listed from the repository for that exact platform) is replaced in the root composer.json before the first install, so vendor never holds the modules and setup:install never enables them. The script asserts on state afterwards: zero inventory directories in vendor, and Magento_InventoryApi absent from config.php. Such a cell advertises no msi capability; see inventory selection for how a manifest asks for it.

Hyvä lines ​

One installation holds one Hyvä version, so each supported line has a cell of its own, placed where it also fills a PHP tuple the policy needs:

CellPlatformPHPHyväState
m246php81hyva12Magento 2.4.6-p158.11.2 (latest 1.2.9)planned: see below
m247php83hyva13Magento 2.4.7-p108.31.3 (Tailwind 3, latest 1.3.23)planned: see below
m248php83hyva14Magento 2.4.8-p58.31.4 (Tailwind 4, 1.4.11 installed)live since 2026-10-28
every other cell1.5live

Composer refuses the two older lines

Composer 2.9 blocks packages with a published security advisory, and Magento 2.4.6 and 2.4.7 require league/flysystem 2.x, which carries one. The first attempt at both cells stopped at composer install for that reason. A cell has to install what the platform release ships, so building them needs the block relaxed for that one package in the cell's composer.json (policy.advisories), which is a deliberate decision rather than something the script does on its own.

bash
FURNACE_HYVA='~1.3.0' scripts/provision-cell.sh m247php83hyva13 magento 2.4.7-p10 8.3

FURNACE_HYVA pins the line. The CSP variant of the default theme exists only from 1.5, so older lines get the default theme alone; the script falls back by itself. A manifest asks for a line with a versioned theme target; see theme selection.

Keeping an instance out of the fleet ​

Any installed MageBox instance under the cell root is a cell. For one you develop in, with modules symlinked into app/code, create an empty .furnace-ignore file in its directory. No furnace command will discover it until the file is deleted.

Which cells are needed ​

config/platforms.yaml is the one deliberate policy file: the vendor-supported platform and PHP tuples that must exist, with the newest patch worth certifying against. It never configures a cell; furnace plan compares it with the facts the cells report and shows what is ready, behind (right release line, older patch) or missing. Override it per invocation with FURNACE_CATALOG.

Goldens ​

Resetting a cell by reinstalling Magento would take minutes. Instead each cell has a golden snapshot: a gzipped dump plus the four files that carry a cell's identity (composer.json, composer.lock, app/etc/config.php, app/etc/env.php). Restoring takes about ten seconds, which is what makes a pristine reset before and after every run affordable. Snapshots live in ~/.furnace/golden/<cell> (override with FURNACE_SNAPSHOTS).

bash
furnace golden                     # what exists, size, age
furnace golden --capture           # capture every healthy cell
furnace golden --capture -c m248   # one cell
furnace golden --restore -c m248   # put a cell back

Capture refuses a cell that fails its health checks, because every later run is restored back to whatever you capture. The checks include golden purity (no leftover extension tables or setup rows, no third-party packages in the root composer.json, no module enabled in config.php without code behind it, developer mode) and golden coherent (below). --force exists and is almost never right.

Capture after changing a cell

The snapshot restores composer.json and composer.lock. Anything installed afterwards is reverted on the next reset, which is how PHPUnit once silently disappeared from two cells. Change a cell, then recapture.

Success is measured by counting CREATE TABLE statements in the dump, never by file size: an empty gzip is 20 valid bytes. A corrupt dump is rejected before the database is dropped. The restore itself is DROP DATABASE, CREATE DATABASE, load: a dump only carries DROP TABLE for tables it contains, so anything an extension created survived a load-over-the-top. Restores hold a lock keyed on the database server, because cells share servers and two concurrent restores deadlock.

Coherence ​

A snapshot restores composer metadata but leaves vendor/ alone. If vendor/ is rebuilt after a capture, restoring the golden produces a cell whose metadata describes one dependency graph while its autoloader holds another, and every gate afterwards measures that instead of the extension.

Each golden therefore records a fingerprint of the dependency graph it was captured against, and a run refuses to start on a cell that no longer matches:

✗ reset  Refusing to restore m248: the golden no longer describes this cell's
         vendor/ (installed packages differ from the snapshot).
         Recapture it: furnace golden --capture -c m248

The fingerprint hashes the set of packages and versions, not the file. Composer rewrites installed.json on any dump-autoload without changing a dependency, and hashing the bytes reported that as drift.

furnace init recaptures a stale golden automatically when nothing else about the cell is wrong: the cell's vendor/ is the truth and the snapshot is what is out of date.

Getting the fleet ready ​

bash
furnace init                 # repair what can be repaired, then verify every cell
furnace init --deep          # and prove each cell can actually run an integration test (~1 min per cell)
furnace init --recapture     # and refresh the goldens of every cell that ends up healthy
furnace doctor               # verify without repairing; exits non-zero on any failing cell
furnace doctor --deep

Run init after a reboot, after an interrupted run, after changing anything about a cell, and before a certification matrix. A red cell is a blocker: a failure produced by a broken cell tells you nothing about your extension.

Doctor checkFails when
databasethe cell's database cannot be reached
platform puritya Mage-OS tree sits inside a Magento cell or the reverse
app/code cleanmodules furnace did not put there (warns; the next run purges them)
golden coherentvendor/ no longer matches the snapshot's package fingerprint
maintenancevar/.maintenance.flag is present, which no database restore lifts
test servicesa service the integration config declares (search, AMQP) does not answer
integration db<cell>_integration_tests does not exist
modethe cell is not in developer mode
golden purityextension residue in tables, setup rows, root requires or config.php
registriesa composer repository the cell declares refuses the credentials composer would send, or has none to send; a registry that is down is a warning
phpunitwrong major for the platform, or an extension class in phpunit.xml that will not autoload
catalog, settingsno sample products; rewrites, store codes or timezone unset
search indexcatalogsearch_fulltext invalid, or an unpatched esconfig.xsd
rewrites[view], renders[view]a store view has no category rewrites, or a real category URL serves no products

Registry credentials ​

Cells install packages from registries (Mage-OS, Hyvä, the vendor's private Packagist). Credentials are composer's own, read exactly where composer reads them: the cell's auth.json, the global auth.json under composer's home, or a COMPOSER_AUTH variable. Furnace stores none.

furnace doctor proves every declared repository answers with the credentials composer would send. furnace init brings a cell's auth.json up to date from the global one, but only for registries the cell declares, and only by replacing a credential the registry refuses with one it accepts. A stale global credential is reported as stale and never copied, so rotating a token is a one-place change and a stale global file cannot break a cell.

The integration sandbox ​

Magento's integration framework builds its own sandbox under dev/tests/integration/tmp/ and reuses it. That sandbox is derived from the module set present when it was built, so reusing it under a different one leaves the compiled DI describing classes the sandbox does not have; PHPUnit then dies in bootstrap and reports zero tests.

Furnace records which modules a sandbox was built for. The same extension reuses it; a different one rebuilds. Clearing it unconditionally instead was correct and cost an extra six minutes per cell.

Furnace 1.0.0-alpha · internal tooling · qoliber.com