config.php set `$CFG->debug = 32767` whenever MODE_DEBUG was true, as
the development and CI inventories set it. Moodle 5.3's
`core_component::is_developer()` treats any debug bit defined in
config.php as developer mode, so `init()` skipped
`$CFG->cachedir/core_component.php` and ran `fill_all_caches()` on
every PHP request: a walk over every plugin directory and every
`classes/` tree. The autoload in setup.php triggers it before the
ABORT_AFTER_CONFIG return, so even font.php and requirejs.php paid it.
On local disk the walk is cheap. In swarm the code tree is an NFS4
mount, and the walk turns into GETATTR round trips: main run
39494396920 served `/theme/font.php` in no less than 1.6 s and
`/lib/requirejs.php` in no less than 2.0 s, the five FPM workers per
replica queued, and the CSP check timed out loading the academy page
after 20 s on both Moodle swarm jobs.
Write `debug` and `debugdisplay` into the site config with
`admin/cli/cfg.php` instead, on every deploy and as integers (32767/1
in debug deployments, 0/0 otherwise). `is_developer()` reads only
values defined in config.php, so the component cache is honoured,
while `setup.php` still applies the stored debug level once the
database is up. The assignments leave config.php entirely, because
cfg.php refuses to change a name config.php defines, and
MOODLE_DEBUG leaves env.j2 with them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pin moved from 4.5.11 to 5.3.0, and the role fitted neither a new
install nor an existing one.
Moodle 5 serves from public/ and routes every other path through
r.php. nginx now uses public/ as its root, hands unknown paths to the
router and passes PATH_INFO for scripts with slash arguments, and
config.php declares the router as configured. The paths the role checks
and writes (version.php, auth, webservice, the OIDC plugin) moved below
public/, the URL of the OIDC plugin stays auth/oidc. The source archive
no longer contains vendor/, so the image installs the Composer
dependencies it declares. The OIDC plugin is pinned to v5.2.1, the
newest release.
An existing site kept its old code: the entrypoint filled the code
volume once and never again. It now records the release it copied and
replaces the tree when the image carries another one, and a new task
runs admin/cli/upgrade.php. Plugins that were installed through the web
interface are not carried over, the README says how to ship them.
A site installed with 5.3 forces login by default, which 4.5 did not,
so its start page answers 303 to the login page and the webserver
health check refused it. The role declares 200 and 303 as the codes
that check accepts: 200 is what an upgraded site keeps answering.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.
The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.
Resolutions that were not unions:
Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.
plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.
compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.
tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.
Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.
01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.
Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.
roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.
config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.
READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.
Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moodle 303-redirects every request whose Host is not $CFG->wwwroot, and an MCP
client dialling the internal service name cannot follow that redirect from the
container network. The previous accommodation was on the wrong side: the probe
sent -H 'Host: <public domain>' and -H 'X-Forwarded-Proto: https' and pinned
mcp_probe_host to MOODLE_DOMAIN, so it asserted the endpoint works when spoofed
and said nothing about how a real client reaches it.
Both headers and the probe host are gone. Acceptance moves into config.php,
which reads MOODLE_INTERNAL_HOST -- rendered from MOODLE_MCP_SERVICE_KEY -- and,
when the incoming HTTP_HOST matches it, rewrites HTTP_HOST and SERVER_NAME to
the wwwroot host before Moodle's redirect logic runs. The comparison splits on
':' so a port cannot defeat the match, and a request from any other host is
untouched, leaving the canonical-host protection intact for real traffic.
None of that could have taken effect on a deployed instance. The bootstrap seeds
the code directory with `cp -an`, which never clobbers, so an existing volume
keeps its first-run config.php forever and every later config change is silently
discarded. moodle_refresh_config now copies the deployment-managed config.php on
every start, which is what makes the rewrite above reachable at all.
Not verified: no MCP call has been observed succeeding against the internal name
since the change. This is also the same failure class that ended the last
deploy on another role, where the client already shared the provider's network
and had the right alias registered, yet the DNS search suffix still resolved the
single-label name to the public ingress. If that applies here too, the Host fix
is correct and the request still never arrives -- and removing the spoofing
headers will surface it as a failure where the probe previously passed.
Two further consequences worth stating: config.php is now overwritten on every
start, so an operator edit inside the container is discarded; and the HTTP_HOST
rewrite runs in the hot path of every page load, narrow though its condition is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A role-files-layout exemption frees a tree from files/<lang>/ because
its import paths are fixed from outside. The mirrored-test lint derived
its scope from that same directory, so every layout exemption silently
became a test exemption nobody ever argued for.
The language now comes from the file suffix. Test code is excluded by
scope rather than exempted: a test-* role and a files/playwright/ or
files/test/ tree are the tests, so demanding a test of them is circular.
The eleven files this newly reaches are framework-instantiated and carry
a stated reason each.
Moodle kept everything under dataroot, which is the shared NFS export in swarm
mode - including localcachedir. Upstream is explicit that this is the one
directory that must not be shared: config-dist.php marks tempdir and cachedir
as 'MUST BE SHARED by all cluster nodes' and localcachedir as 'Intended for
local node caching'.
The path is declared once, as the mount target in meta/volumes.yml, and every
other place derives from it: vars/main.yml reads it back through
lookup('volume', ...), compose passes it as a build argument, the Dockerfile
exports it, the entrypoint requires it with :? and creates it with the runtime
owner, env.j2 hands it to config.php as MOODLE_LOCALCACHEDIR. The two mounts of
the same volume share one YAML anchor rather than repeating the literal.
The cron service mounts it too, because it runs the same code and would
otherwise write its cache somewhere else than the web service reads it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>