fix(deploy): deliver env: via ePHPm's per-site auto_prepend_file (#4) - #27
Merged
Merged
Conversation
A manifest's `env:` never reached PHP for an app declaring `docroot: "."`.
switchboard wrote `.ephpm-preview-prepend.php` into the checkout and announced
it in a sidecar ePHPm does not read, so `ini_get('auto_prepend_file')` on a
live preview returned `''`. Apps had to `require_once` it by hand — which
`docroot: "."` apps mostly did not, so `EPHPM_SEED_TOKEN` never arrived and
wordpress-sample came up as stock WordPress instead of the magazine showcase.
ephpm#463 (PR #472) made `auto_prepend_file` a typed, enforced key in the same
per-site override file switchboard already writes for `document_root`. This
writes it.
The mechanism
* The override is now written for **every** preview, not only for a
subdirectory docroot. `docroot: "."` is spelled by omitting `document_root`
(absent and `"."` are identical to ePHPm, and absent is the spelling every
release agrees on) — the file still has to exist, because it carries the
prepend, and that shape is exactly the one with nowhere else to put a
bootstrap hook.
* `validate_prepend` mirrors ePHPm's `validate_declared_prepend`: relative,
`Component::Normal` only, no backslash, charset-gated, canonically contained,
and a regular file. The deploy fails loudly on a value ePHPm would refuse —
since #472 a refusal is a 503 for that site, not a silent no-op.
* `write_override` is now atomic (temp + `sync_all` + rename). A plain `write`
truncates first; interrupted, it leaves valid-looking or unparseable TOML
live, and ePHPm re-reads the file every couple of seconds. Under #472 that
window is a down site rather than a warning.
* The prepend is unlinked before it is written, so a repository shipping a
symlink at that name cannot have the preview's resolved secrets written
through it.
* The prepend refuses to overwrite `$_SERVER['DB_*']` / `$_SERVER['EPHPM_*']`.
It now runs on every request, after ePHPm injects this vhost's live,
rotating credentials — an `env:` entry with a colliding name would have
replaced a working credential with a stale literal. It still lands in
`$_ENV`/`putenv()`, and the deploy warns naming the keys.
The `.env` from #14 stays, as a distinct mechanism rather than a duplicate
`auto_prepend_file` exists only inside an ePHPm request. `build:` and `seed:`
run as shell commands outside the server (guide §8), where a `wp-cli` or
`artisan` step can only read `env:` from a file. `.env` is also what carries
`env:` on a host predating #472. Both are rendered from the same resolved map
in one function, so they cannot disagree about values, and the prepend wins
where both apply (it runs first, and phpdotenv/symfony-dotenv are immutable by
default). The filename is deliberately unchanged, so the documented manual
`require_once` keeps working and is a no-op rather than a conflict.
Deploy ordering
The nodes run ePHPm 0.9.0, which predates #472 and ignores `auto_prepend_file`
with a warning — by design, so a switchboard deploy cannot take every preview
site down. So switchboard may ship first: in the window, `document_root` is
still honoured, sites keep serving, and `env:` arrives via `.env` and the
manual require exactly as today. The feature turns on when the nodes upgrade
to an ePHPm carrying #472. Nothing here requires a coordinated deploy.
Closes #4
luthermonson
force-pushed
the
fix/auto-prepend-file-per-site
branch
from
September 8, 2026 03:03
a0f35f2 to
2513c76
Compare
|
ePHPm Preview — removed Preview deployment has been torn down. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4.
A manifest's
env:never reached PHP for an app declaringdocroot: ".".switchboard wrote
.ephpm-preview-prepend.phpinto the checkout and announced itin
.switchboard-preview.json— a file ePHPm does not read — soini_get('auto_prepend_file')on a live preview returned''. Apps had torequire_onceit by hand, whichdocroot: "."apps mostly did not, soEPHPM_SEED_TOKENnever arrived andephpm/wordpress-samplecame up as stockWordPress instead of the magazine showcase.
ephpm#463 (PR #472,
c3cdcffon ephpm main) madeauto_prepend_filea typed,enforced key in the same per-site override file switchboard already writes for
document_root. This is switchboard's half.The mechanism
The override is written for every preview, not only for a subdirectory
docroot. It used to be removed for
docroot: "."on the reasoning that anabsent file and "the container is the web root" are the same thing to ePHPm.
They are — for routing. They stopped being the same when the file gained
auto_prepend_file, anddocroot: "."is precisely the shape with nowhereelse to put a bootstrap hook.
docroot: "."is now spelled by omittingdocument_root(absent and"."are identical to ePHPm, and absent is thespelling every release ever shipped agrees on).
validate_prependmirrors ePHPm'svalidate_declared_prepend: relativeonly,
Component::Normalsegments only, no backslash, charset-gated(
[A-Za-z0-9._/-], because we hand-write the TOML), canonically contained inthe container, and a regular file. Resolved against the staging tree —
the tree that becomes the container. A value ePHPm would refuse fails the
deploy, because since #472 a refusal is a 503 for that site, not a no-op.
write_overrideis now atomic — temp file,sync_all,rename. It wastokio::fs::write, which truncates first. ePHPm re-reads this file everycouple of seconds, and under #472 an unparseable read takes the site out of
service, so the truncate window went from "a warning" to "a down preview". The
sync_allmatters separately: a rename durable before its data leaves azero-length override, which is valid TOML declaring nothing — the site would
come back serving its whole container with no error anywhere. The temporary is
dot-prefixed and
.tmp-suffixed so it can never be read as<key>.toml, andcarries the pid so two writers cannot share one.
The prepend is unlinked before it is written.
writefollows a symlink, soa repository shipping one at that name would have the preview's resolved
secrets written through it to wherever it points. Removing the entry replaces
the link, not its target.
The prepend no longer overwrites
$_SERVER['DB_*']/$_SERVER['EPHPM_*'].This is a new hazard the fix itself introduces: the prepend now runs on every
request, after ePHPm's
register_server_variablesinjects this vhost's liveper-site credentials — which rotate on every host restart. A manifest
declaring
env: { DB_HOST: ... }would have replaced a working credentialwith a stale literal. The declared value still lands in
$_ENV/putenv(), soan app wanting an external database can read it there, and the deploy warns
naming the keys.
$_SERVERvsgetenv()docs/preview-app-guide.md§2 tells app authors to read$_SERVER: ePHPm has nosapi_module.getenvhandler, everything arrives viaregister_server_variables,and phpdotenv's
DEFAULT_ADAPTERSconsultsServerConstAdapterfirst. Theprepend was already consistent with that — it sets all three,
$_SERVERincluded — and stays so. The one change is the guard above, which the guide now
documents.
What happened to the
.envfrom #14 — kept, as a different mechanismTwo mechanisms doing one job drift; these do two jobs.
auto_prepend_fileexists only inside an ePHPm request.build:andseed:run as shell commands outside the server (guide §8: no
$_SERVER['DB_*'], noprepend), and a
wp-cliorartisanstep there can only readenv:from afile. That is
.env, and no auto-prepend can ever cover it..envis also whatcarries
env:on a host predating #472.They cannot disagree: both are rendered from the same resolved
BTreeMapin thesame function. And there is no precedence to reason about — the prepend runs
before the front controller, and phpdotenv/symfony-dotenv are immutable by
default, so a
$_SERVERvalue set first is not overwritten.The filename
.ephpm-preview-prepend.phpis deliberately unchanged, so everyapp that adopted the documented
require_oncekeeps working. Keeping that lineis harmless — the prepend only assigns values, so twice and once are the same
outcome.
Deploy ordering, and the skew window
Either side may ship first; switchboard-first is safe. Confirmed read-only on
the live cluster today:
ephpm 0.9.0,site_overrides_dirwired at/var/lib/ephpm-web/site-overrides, currently empty (every deployed preview isdocroot: ".", which today's code deletes the override for — which is the bug).v0.9.0 predates #472, so it treats
auto_prepend_fileas an unknown key:ignored, with a warning, site keeps serving. #472 kept unknown keys lenient
for exactly this reason. In the window:
document_rootis still honoured — unchanged behaviour for Laravel-shapedpreviews;
env:arrives via.envand the documented manualrequire_once, exactly asit does today;
names the ePHPm issue).
The feature turns on the moment the nodes run an ePHPm carrying #472. No
coordinated deploy, no flag day.
What an operator must do
env:into PHP: upgrade the three preview nodes to an ePHPmcontaining #472 (
c3cdcffor a release cut after it). Redeploy a preview(or wait for the next PR event) so the override is rewritten with the key.
ini_get('auto_prepend_file')on a preview should be an absolute pathinside the site container, not
''.ephpm/wordpress-sampleshould come upas the magazine showcase.
--site-overrides-dirwas already required fordocroot:; it is now alsowhat delivers
env:. Unset, the deploy warns about both consequences inone line instead of only the docroot one.
[php] mode = "worker") has no per-request prepend position;ePHPm ignores the key there with a warning. Documented in the guide's gaps
table. The preview fleet is per-request.
Tests
210 passed,cargo fmt --all -- --check(stable, what CI runs) andcargo clippy --all-targets -- -D warningsclean.New coverage, in
src/site_override.rsandsrc/deployer.rs:the_generated_prepend_name_satisfies_ephpms_containment_rules— asserts theshipped constant against each of ePHPm's rules by name (relative,
Normalcomponents, no backslash, dot-prefixed) and then through
validate_prepend.This is the "getting it wrong is a 503" test.
prepend_escaping_the_container_is_refused— traversal, absolute, UNC, drive,empty and
..prepend_symlinked_out_of_the_checkout_is_refused.prepend_naming_a_directory_or_a_missing_file_is_refused,prepend_toml_injection_attempts_are_refused.a_prepend_above_the_document_root_is_accepted— containment is against thecontainer, and the file is genuinely outside the served root.
docroot_dot_still_gets_an_override_carrying_the_prependanda_root_docroot_preview_gets_an_auto_loaded_prepend— the env: never reaches PHP for docroot: "." — the generated prepend is not auto-loaded #4 shape end to end.the_override_is_replaced_atomically_leaving_no_temp_files— a rewritereplaces rather than accumulates, and no
.tmplitter survives.container_docroot_clears_a_stale_document_root_but_keeps_the_override—replaces the old "clears a stale override" test, which asserted the behaviour
this PR deliberately changes.
the_prepend_never_clobbers_a_host_injected_credential.Also
teardown.rsnow derives the override path throughsite_override::override_path/
remove_overrideinstead of re-joining<key>.tomlitself, so one moduleowns the one filename ePHPm reads.
"prepend_auto_loaded": falseis replaced by"prepend_declared_as".falseis no longer true on a current host, andtruewould be a claim about a server version this daemon has no channel toask about. What it records is that the declaration was made, and as what.
limitation), README updated in three places.