Appearance
Install methods
How the extension's code reaches a cell is declared in the manifest's install section. For one run, --install=composer|copy overrides the method, --source=path|archive|packagist the source, and --release=1.2.0 installs that released version from the registry.
| Setting | What happens |
|---|---|
method: composer, source: path | Each module is staged into ~/.furnace/work/<cell>/stage with export-ignore rules stripped so Test/ survives, a path repository is prepended to the cell's composer.json with symlink: false and a version for every package, and composer require installs them. The gate passes only when every package is in vendor/composer/installed.json, from that repository, at that version. |
method: composer, source: archive | Same, but each package is what composer archive would ship, so export-ignore and autoload mistakes surface. The repository's own Test/Integration runs against the installed package. |
method: composer, source: packagist | composer require of released versions from the repositories the cell knows, optionally pinned with version. Tests come from the installed package when it ships them, else from the checkout, and the gate warns unless HEAD is at a clean tag equal to the installed version. |
method: copy | The legacy path: modules are rsynced into the cell's app/code with an ownership marker, and third-party requirements the cell lacks are added by hand. Conformance warns about it, because it never tests the package a customer installs. |
Modules without a version in their composer.json are given one for the run only: the highest lower bound a sibling module places on the name, then the nearest git tag, then 1.0.0. Nothing is written into the extension's own files.
Symlinks are never used. Magento's template validator resolves real paths and refuses templates outside the cell, late, at page render, and the escape hatch (dev/template/allow_symlink) is documented unsafe for production, which the production gate runs.
Teardown restores the golden's composer.json and composer.lock, runs composer install with plugins and scripts disabled, verifies the package fingerprint, removes the stage and restores the database. The cell comes back exactly as it was, and doctor can prove it. Never composer remove.
Why composer and not a copy
Copying into app/code never tested the artefact customers install. With composer require the solver evaluates every declared constraint, pulls the companions and third-party packages the extension declares, and fails for exactly the reasons a customer install would. On the first run of m2-faqs this way the solver pulled openai-php/client, php-http/multipart-stream-builder and qoliber/hyva-module-registration, the dependency-leak class the copy path never saw.
When a run fails at composer require, the first Problem line in install-composer-require.log under the run directory is the customer's install error too.
Testing a release
After tagging and publishing, test what customers receive: check out the tag and run with --release. The committed manifest keeps saying path, because it describes the repository, and a release test is a property of one run.
bash
git checkout 1.2.0
furnace run --gate=all --release=1.2.0 # qoliber/foo:1.2.0 from the registry, this checkout's tests
furnace run --gate=all --source=packagist # the newest release insteadPublic Packagist, Private Packagist, Satis and GitLab package registries are all composer repositories and work the same way. The cell must know the repository: cells provisioned with the script carry Mage-OS, Hyvä and the vendor registry, and any composer repository declared in the extension's root composer.json is added for the run. Credentials are composer's own (see cells); furnace stores none. Removal is the same teardown as above: a release run leaves no package behind.
The install gate compares the checkout with what was installed and attaches a warning when HEAD is not at a tag matching the installed version, when the tree has uncommitted changes, or when the directory is not a git repository. The warning is recorded in the report so a reader knows which tests judged which code.
A release is judged by the tests it shipped: when the installed package carries Test/Integration, those tests run against the installed code. A 1.0.0 release of m2-faqs was found this way to fail two of its own controller tests that the current checkout no longer fails. That is the release's finding, not furnace's.
No checkout at all
When the thing under test is a set of released packages and there is no checkout at hand, a release harness does the same from a manifest alone.
Traps
- Composer package names are lowercase. A
Vendor/Nameininstall.packagesis a schema error, not a case-insensitive match. - A path repository's
options.versionsoverrides a declared version. Furnace relies on this to give unversioned modules a version for the run. - A non-glob path repository URL that no longer exists makes every composer command throw. The stage is therefore removed only after the golden
composer.jsonis back. - A suite-shaped extension (the repository root is
app/code/Vendor) has no rootcomposer.json, so no repositories are added from it; the cell must already know the registry.