Skip to content

How a run works ​

A run has two phases. The static phase happens once, before any cell is touched. The cell phase repeats for every matching cell.

Static phase ​

gateasserts
conformancethe manifest loads, declared layers exist, no forbidden configs are shipped, seed tokens match; recorded, and refused only with --require-conformance
rectorthe code needs no changes to run on its declared PHP floor; a dry run that never writes source
syntax floorevery PHP and PHTML file parses on every required PHP minor; a missing interpreter is incomplete, not compatible
static analysisthe extension's own PHPStan; PHPCS against its config, or Magento's Magento2 standard as advisory
unitthe extension's unit tests from the checkout

A failure here stops the run before a single cell is touched. That is the point: a broken unit test should cost a second, not the six minutes it takes to reach the integration gate.

Why PHPCS is advisory

When an extension ships its own phpcs.xml it has opted into a standard, and breaking it fails. When it ships none, furnace falls back to Magento's Magento2 standard and only reports. Imposing a standard the author never adopted, on every gate that follows, is furnace making policy rather than checking work.

A release harness has no source of its own, so this tier is reported n/a here and runs instead on every cell, after composer has installed the packages.

Cell phase ​

Gates run as a ladder. Each one runs only if the previous held, because a green production build on a cell whose tests failed is not evidence of anything.

gatewhat happens
fitnessthe cell's health checks; a cell left dirty by an earlier run is recovered from its golden first
resetrefuse if the golden no longer describes the cell's vendor/; empty app/code and app/design; drop and recreate the database from the golden; clear generated/, caches, pub/static output and the maintenance flag
composer requirethe package route: one composer require of the staged, archived or released packages, verified by state in installed.json
dependencies + copythe legacy route: install composer requirements the cell lacks, then rsync the modules into app/code with an ownership marker
setup:upgradeapplies, and setup:db:status reports nothing pending
setup:upgrade againmust be a no-op; patches that re-apply break every customer upgrade
modules enabledevery module registered and enabled
di:compilecompiles
rector, syntax floor, static analysis, unitharness only: the source gates, run on the packages composer just installed, with the cell's tools and PHP
cronevery declared job made due and run to success; n/a when none are declared
integrationPHPUnit per suite directory; every declared test method must have executed
seedthe seeder runs inside the cell and reports the tokens it produced; a failed seed stops the ladder
productionreindex, static content, production mode, both store views still serve products, then back to developer mode (and that return must succeed)
e2e[view]Playwright per selected store view and role, from a private workspace, against the cell's own URL
cleanupgolden Composer metadata back, composer install, fingerprint verified, database restored

Explicit manifest layers select the gates. Omitted layers are n/a; disabled required gates and skipped or incomplete PHPUnit tests prevent certification.

Browser suites run in private copies. Generated configs, admin state, JSON results and traces live with the run logs. Login setup tests do not count as extension coverage, and seed tokens come from the seeder's actual output.

The other ladder: a fresh install ​

--fresh-install replaces everything after composer require or copy with one gate: remove app/etc/config.php and app/etc/env.php, drop and recreate the database, run setup:install with the extension in place, then check modules, schema and the home page. No upgrade, tests or production build run. It is the install path a customer takes on day one, and an extension can pass one ladder and fail the other. Fresh install has the details and a live result.

Details that are not obvious ​

setup:upgrade runs twice. The second run must report nothing pending. An extension whose schema or data patches are not idempotent breaks every upgrade its customers perform, and only a second run reveals it.

Cron is made due, not waited for. A */5 job is scheduled at the next five-minute boundary, so waiting would make the result depend on the wall clock. Furnace gives each declared job a row due now and runs it. Dispatch still goes through Magento's real cron path.

Integration runs one invocation per suite directory. PHPUnit 9 takes a single path argument and silently ignores the rest, so passing five directories tested one of them and reported a clean pass.

Every declared test must run. The gate counts the test methods your suites declare and fails if fewer executed. A runner quietly executing a subset is indistinguishable from success, which is how the above went unnoticed.

Integration tests come from the installed package when it ships them. For an archive or registry install, the package's own Test/Integration runs against the installed code, and the checkout's tests are the fallback. A release is judged by the tests it shipped, which is what a release pipeline does.

Production always hands the cell back in developer mode. The return is a step of its own and it must succeed; a fleet cell left in production mode makes the next extension's failures unreadable.

Furnace 1.0.0-alpha · internal tooling · qoliber.com