Skip to content

Self-bootstrapping bundled dev env at the extension root#811

Open
alistair3149 wants to merge 20 commits intomasterfrom
dev-env-redesign
Open

Self-bootstrapping bundled dev env at the extension root#811
alistair3149 wants to merge 20 commits intomasterfrom
dev-env-redesign

Conversation

@alistair3149
Copy link
Copy Markdown
Member

@alistair3149 alistair3149 commented May 5, 2026

Converts Docker/ from a try-it-out-only stack into the canonical NeoWiki
dev environment, with a single make entry point at the extension root.
A fresh clone now goes from zero to a running wiki with make dev.

Summary

  • New dev-mw Dockerfile stage and docker-compose.dev.yml overlay with a
    bind-mounted NeoWiki source, MW_MODE=dev-aware SettingsTemplate.php,
    and an opt-in LocalSettings.local.php hook.
  • make dev, make phpunit, make cs, make tsci, make bash,
    make reset, etc. live at the extension root in a single dual-mode
    Makefile (proxies via docker compose exec from the host; runs binaries
    directly inside the container).
  • make bootstrap clones MW core into the gitignored Docker/mediawiki/,
    so a public clone is self-bootstrapping.
  • Auto-allocated host ports from documented ranges: MW_SERVER_PORT
    8484-8499, MAILCATCHER_PORT 8025-8040. make dev-tools opt-in adds
    Neo4j Browser/Bolt at 7474/7687.
  • Docker/.env now ships as Docker/.env.dist and is auto-copied on
    first run; runtime port allocation no longer dirties the working tree.
  • opcache.validate_timestamps=1 re-enabled in the dev image so PHP
    edits (including LocalSettings.local.php overrides) land on the next
    request without a container restart.

Test plan

  • Fresh clone, no prerequisites: make dev succeeds end-to-end and
    http://localhost:8484/index.php/Main_Page renders.
  • make phpunit filter=SchemaNameTest, make phpcs, make ts-lint
    pass.
  • make dev-tools exposes Neo4j Browser at http://localhost:7474.
  • Override port=8488 correctly allocates MW_SERVER_PORT=8488 while
    keeping MAILCATCHER_PORT in its 8025-8040 range.
  • Edits to Docker/LocalSettings.local.php propagate to the running
    container within ~2s, no restart needed.
  • make stop / make down / make reset work.

alistair3149 and others added 18 commits May 5, 2026 13:35
Provides a dev-capable image that does not bake NeoWiki source.
NeoWiki is mounted at runtime; composer install runs on first boot.
Includes vim/nano/less/make for in-container debugging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The entrypoint runs composer install as PID 1 (root). Without this
fix, vendor/ files are root-owned on the host bind-mount and developers
need sudo to clean them up. Make the directory www-data-owned and
world-writable so cleanup works without elevated privileges.

For routine re-installations after first boot, prefer
`make composer-install`, which runs as the host UID via
`docker compose exec --user` and produces host-owned files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds test_neo (parallel test isolation on bolt 7689 / http 7475),
node (TS watcher sidecar against /workspace bind-mount), and
mailcatcher (SMTP catcher on MAILCATCHER_PORT) to the dev profile.

Documents the v2 local-backend profile placeholder above db.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review feedback:

- node sidecar: restart: unless-stopped causes tight restart loops on
  npm install failure. Switch to on-failure:3 so transient errors still
  recover but persistent ones halt.
- test_neo: NEO4J_AUTH is hardcoded because NeoWiki's test suite
  expects a fixed credential. Add a comment so this is not "fixed" later
  without also updating tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches mediawiki service to locally-built dev-mw target,
bind-mounts the NeoWiki source, optionally overlays sister
extensions via MW_EXTRA_EXTENSIONS_DIR.
When MW_MODE=dev, enables exception details, debug SQL, disables caches,
and points SMTP at mailcatcher. Defaults preserve production behavior.

Adds LocalSettings.local.php opt-in extension point for per-worktree
overrides (e.g., loading sister extensions).
MW_MODE for dev/production switch, MAILCATCHER_PORT, MW_EXTRA_EXTENSIONS_DIR
for sister-extension overlay, SHARED_BACKEND placeholder for v2.
…ults

Adds make dev, bash, phpunit, cs, tsci, composer-install, ts-install,
ts-build, reset, update-dot-php. Derives COMPOSE_PROJECT_NAME from
parent directory for collision-free worktree parallelism. Auto-allocates
MW_SERVER_PORT on first run. Health-gates make dev on /Special:Version.
Closes the public contributor onboarding gap for issue #120.
Documents make dev, test/lint targets, per-worktree behavior,
and LocalSettings.local.php opt-in.

Also gitignores Docker/LocalSettings.local.php so the per-worktree
opt-in file stays untracked.
Public contributors and PW devs now run 'make dev', 'make phpunit',
'make tsci' etc. directly from mediawiki/extensions/NeoWiki/. The
Docker/ directory keeps the stack files (Dockerfile, compose, scripts)
but no longer needs its own Makefile.

PHP test targets (phpunit/cs/stan/psalm) are dual-mode: when invoked
from outside the container they exec via docker compose; when invoked
from inside the container (e.g. by the existing AGENTS.md docker
compose exec wrapper) they run the binaries directly. TS targets
exec into the running 'node' sidecar.

Project name is lowercased to satisfy compose v2's naming rule.
The set-port.sh script honours an ENV_FILE env var so it can write
to Docker/.env from the extension root.
The dev-mw Dockerfile stage now bakes SettingsTemplate.php as
LocalSettings.php (matching final-mw) so install-db's swap dance
has something to swap with on first boot.

The _wait-mw health gate now waits for any HTTP response rather
than grepping for 'NeoWiki|Jeroen', because before install-db
runs the wiki redirects to the setup page.

Drops ts-install / ts-build from _first-run-seed: the node sidecar
already runs npm install && npm run build:watch on startup, so
making the seed call them in parallel would race on file locks.
Port 1080 is commonly reserved on Windows hosts (SOCKS proxy) and
Docker Desktop on WSL2 fails to bind to it. 1180 is unreserved.
PHPUnit (and other PHP tools to a lesser degree) hangs at startup when
invoked via 'docker compose exec -T' because that mode does not fully
close stdin. The MW test bootstrap blocks reading from it. Adding
'< /dev/null' to the inside-container invocations forces a closed stdin.

Also relaxes the _wait-mw probe to use 'curl -sS' instead of '-fsSL',
so the gate accepts 5xx responses (expected before install-db has
created the wiki tables on first boot).
Each worktree's mailcatcher used to bind the default 1180 host port,
which collides as soon as a second worktree starts. Derive the
mailcatcher port from MW_SERVER_PORT (offset from 8484/1180), so each
worktree gets its own MW + mailcatcher pair without a separate
allocation pass.
- Add a bootstrap make target that clones MW core into Docker/mediawiki/
  (the gitignored build prerequisite) and seeds Docker/LocalSettings.local.php
  on the host. make dev depends on it, so a fresh clone is now self-bootstrapping.
- Bind-mount Docker/LocalSettings.local.php into the container instead of
  expecting the entrypoint to materialize it inside. The override mechanism
  documented in the README now actually works.
- Replace the MW_EXTRA_EXTENSIONS_DIR /dev/null fallback with a tracked empty
  directory (Docker/empty-extensions-extra/), so the bind mount is always
  valid even on Compose versions that reject device files as bind sources.
- Stop tracking Docker/.env: it ships as Docker/.env.dist and the Makefile
  copies it on first run. set-port.sh's runtime port writes no longer show
  up in git status.
- Drop the RANGE_START carve-out in set-port.sh; trust is_port_free to
  handle 8484 like any other port.
- Apply '< /dev/null' to the outer docker-compose-exec wrapper for all dual-
  mode targets so the docker-exec stdin hang fix matches the documented
  mitigation.
- Use the lightweight 'docker --version | grep podman' check instead of
  'docker info', which is slow and can hang briefly when the daemon is busy.
- Add _wait-node prerequisite to ts-* targets so the first invocation after
  make dev does not race the node sidecar's initial 'npm install'.
- Flag the dev-mw image as DEV-ONLY in the Dockerfile and document the
  build-time composer-merge-plugin caveat (NeoWiki-declared root-level deps
  would not get merged at build time).
- Trim dead code from dev-entrypoint.sh now that LocalSettings.local.php
  always exists via the host bind mount.
- Generalize 'PW internal devs' wording in compose/.env.dist/Makefile to
  vendor-neutral language (this is a public repo).
- README example uses a generic 'SomeExtension' instead of a vendor-specific
  extension name.
…rst start

The build-time composer install in the Dockerfile happens before the NeoWiki
bind-mount exists, so composer-merge-plugin cannot pull
extensions/NeoWiki/composer.json into the root vendor. On a fresh-clone
make dev, MediaWiki's autoloader then could not find NeoWiki's runtime
dependencies (jeroen/file-fetcher, opis/json-schema, etc.) and demo-data
import failed with 'Class FileFetcher\\SimpleFileFetcher not found'.

The entrypoint now runs composer update at MW root once on first start
(idempotent via a marker file in vendor/) so the lock regenerates with
the merged include list and NeoWiki's deps land in the root vendor.

Discovered while smoke-testing the bootstrap path against a wiped state
(no Docker/mediawiki, no .env, no LocalSettings.local.php, no volumes,
no image). After this fix the full make dev sequence succeeds end-to-end.
- set-port.sh: allocate MW_SERVER_PORT (8484-8499) and MAILCATCHER_PORT
  (8025-8040) from independent ranges. Previously mailcatcher was derived
  from the MW port, so an explicit `port=9000` pushed mailcatcher to 1696.
  Each port is now is_port_free-checked under a single lock taken once
  for the whole pass.
- Default MAILCATCHER_PORT bumped from 1180 to 8025, matching the
  MailHog/mailpit convention used elsewhere in the dev tooling ecosystem.
  1180 was a workaround for 1080 (SOCKS) being commonly bound on
  Windows/WSL2; 8025 sits in the 8xxx dev-tooling cluster.
- Add an opt-in Docker/docker-compose.tools.yml overlay that exposes
  Neo4j Browser (7474) and Bolt (7687) to the host. Reachable via
  `make dev-tools` from the extension root or `make dev-tools` from
  the worktree-wrapper Makefile. Single-worktree by default; override
  NEO_BROWSER_PORT / NEO_BOLT_PORT for parallel tools-mode worktrees.
- Document the reserved host port ranges in Docker/README.md and link
  to it from the extension README and AGENTS.md.
- Replace the hardcoded `http://localhost:8484` references in the
  extension README with explicit notes that the port is auto-allocated.
@alistair3149 alistair3149 requested a review from malberts May 5, 2026 21:45
@alistair3149 alistair3149 marked this pull request as ready for review May 5, 2026 21:45
fs_overlay/usr/local/etc/php/conf.d/opcache-recommended.ini sets
opcache.validate_timestamps=0 (production tuning) and is shared by every
image stage. In dev that means PHP edits to bind-mounted files are never
picked up without a container restart, including LocalSettings.local.php
overrides — the documented per-worktree extension point.

A dev-only conf.d drop-in (zz-dev-opcache.ini) flips the flag back on for
the dev-mw stage. Verified empirically: with the fix, edits to
LocalSettings.local.php propagate within revalidate_freq (~2s); without
the fix, the same edit was still invisible after 30s.

Also clarifies the docker-compose.dev.yml comment: LocalSettings.local.php
is what users edit; SettingsTemplate.php stays baked.
@JeroenDeDauw
Copy link
Copy Markdown
Member

Interesting approach

Seems like a mediawiki clone ends up being on the host fs, which is great for IDE autocomplete and AI grep. However composer dependencies added to vendor/ with composer install in the Docker image build won't be there. This adds some friction. Not a deal breaker for MVP.

@JeroenDeDauw
Copy link
Copy Markdown
Member

JeroenDeDauw commented May 5, 2026

PR LGTM, though I have not tried it yet.


Result of back and forth with @JeroenDeDauw.
Context: this PR diff plus an inspection of the MW REL1_43 submodule layout and the dev-entrypoint flow.
Written by Claude Code, Opus 4.7

Goal: a fully-populated MW vendor/ on the host filesystem.

Today the relationship between host and container is one-way for MW core: Docker/mediawiki/ is only a build context. The Dockerfile's composer install writes into the image, and the dev-entrypoint's composer update (which merges NeoWiki deps via composer-merge-plugin) writes into the container's writable layer. Neither flows back to host. The host's mediawiki/vendor/ stays at whatever the WMF deployment submodule provides — production deps only, no dev deps, no post-merge state.

This matters for IDE autocomplete and for AI tooling that reads what's on disk. Both work much better when host vendor matches what the container actually runs.

Possible fix, kept consistent with the self-bootstrapping design:

  1. Add a host-side composer install to `make bootstrap`, run via `docker run composer:2` so no host PHP is required. ~30s one-shot, idempotent. Gives host the full vendor including dev deps.
  2. Optional follow-up for drift: a `make sync-mw-vendor` target that re-runs (or `docker cp`s) when an MW-root dep changes. Or bind-mount `Docker/mediawiki/vendor/` into the container so subsequent composer ops write through to host — bidirectional for vendor only, MW core PHP stays image-baked.

Both are small and orthogonal to the rest of the Makefile. Happy to do as a follow-up rather than block this PR.

Host's Docker/mediawiki/vendor/ used to be whatever MW's REL1_43 git
provided (the WMF deployment submodule, production deps only). The
container's composer install/update wrote into image layers, never
visible to the host. IDE autocomplete, AI grep, and any host-side
static analysis saw a partial vendor that did not match what MW was
actually loading.

Bind-mount /var/www/html/w/vendor onto the host at Docker/mediawiki/vendor/
so the dev-entrypoint's composer update (with composer-merge-plugin
pulling in NeoWiki's deps) writes through. After first boot the host
sees the full post-merge vendor — including jeroen/file-fetcher,
laudis/neo4j-php-client, opis/json-schema, and the dev tooling.

Drop the now-redundant 'composer install' from the dev-mw Dockerfile
stage: the bind mount shadows that vendor anyway, and the entrypoint
already runs composer update on first boot. Saves ~30s of image build.

Entrypoint chowns the bind-mounted vendor to www-data and chmods
ugo+rwX so the host user can clean up despite cross-namespace UIDs,
matching the existing NeoWiki-vendor handling.

Verified: 52 packages on host vendor after first boot, including
laudis/neo4j-php-client and jeroen/file-fetcher (the merge-plugin
contributions). Marker file at vendor/.neowiki-merged correctly
suppresses re-running composer update on subsequent boots.

Discussed in #811 (comment)
@alistair3149
Copy link
Copy Markdown
Member Author

alistair3149 commented May 6, 2026

Back and forth with @alistair3149 → simplified to bind-mount only.
Claude Code, Opus 4.7

Done in 32decc0: bind-mount Docker/mediawiki/vendor/ to host so the entrypoint's existing composer update writes through. Dropped the dev-mw composer install from the Dockerfile (bind mount shadows it). chown/chmod for host-deletability, matching the NeoWiki-vendor pattern.

Verified after a clean first boot: 52 packages on host vendor, including laudis/neo4j-php-client and jeroen/file-fetcher. composer.lock stays in-image (MW REL1_43 doesn't track one).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants