Skip to content

chore: parallel local dev — isolated WP instances per worktree - #129

Draft
dmytrobez wants to merge 3 commits into
futurefrom
chore/parallel-local-dev
Draft

chore: parallel local dev — isolated WP instances per worktree#129
dmytrobez wants to merge 3 commits into
futurefrom
chore/parallel-local-dev

Conversation

@dmytrobez

@dmytrobez dmytrobez commented Aug 3, 2026

Copy link
Copy Markdown

Replaces #128, which targeted main. The scripts assume the next/ subdirectory layout that only exists on future, so this is rebased onto it.

What

Infrastructure to run several isolated local WordPress instances in parallel — one per git worktree — so multiple people (or agents) can work side by side without port / database / container-name conflicts.

  • wordpress/docker-compose.yml — parameterised via env (PROJECT_CODE, WP_PORT, DB_PORT, WORDPRESS_URL, NEXT_URL, DEV_AUTOLOGIN_SECRET). Each instance gets its own compose project, ports, database and containers; WP_HOME/WP_SITEURL follow the instance URL so a seeded database works on any port. Every variable is defaulted — with no .env, behaviour is identical to today (:80, :3306, project spck).
  • scripts/create-worktree.sh <n> [ref] — creates a detached worktree ../<repo>-wt<n>, copy-on-write clones the heavy gitignored dependency directories, then runs setup.
  • scripts/setup-instance.sh <n> — generates wordpress/.env, wordpress/wp-cli.local.yml and next/.env. Instance N gets 808N / 3306N / 310N, or fails if one is occupied.
  • wordpress/scripts/db-seed.sh <source-checkout> — copies another instance's database and uploads into this one, rewriting stored URLs to this instance's ports.
  • wordpress/scripts/instance-lib.sh — shared env loading and the container-ownership guard.
  • wordpress/server/mu-plugins/dev-autologin.php — local-only auto-login helper.

Guide: docs/parallel-local-dev.md, linked from the README.

Usage

sh scripts/create-worktree.sh 1
cd ../<repo>-wt1
(cd wordpress && npm install && npm run build)
(cd wordpress && npm start)
sh wordpress/scripts/db-seed.sh /path/to/main-checkout
(cd next && PORT=3101 npm run dev)

Why it's safe on staging/production

  • The only file that runs inside WordPress is the auto-login mu-plugin, and it is inert unless DEV_AUTOLOGIN_SECRET is set — which only the local docker-compose.yml does.
  • Nothing in the deploy path ships it: the Dockerfile copies only server/custom.ini and server/wp-su.sh, and .github/actions/theme-deploy rsyncs only wordpress/theme/.
  • All docker-compose.yml / start-wp.sh changes are local-dev only and fully backward-compatible (defaulted env), so the standard single-instance workflow is untouched.

Review

Three independent adversarial reviews (safety, simplification, shell/POSIX rigour) were run against the diff. All findings that survived verification are fixed:

Safety

  • start-wp.sh had no ownership check. Two checkouts defaulting to PROJECT_CODE=spck share a compose project identity, so docker compose up in the second recreates the first's running containers rather than clashing with them. The guard existed but was wired only into the database scripts. Verified against a real cross-project collision on a dev machine.
  • The guard failed open when the compose ownership label was absent, and compared against pwd while Docker stores the symlink-resolved path (pwd -P).
  • db-seed.sh replaced the WordPress URL before the Next.js one. A bare http://localhost source URL is a prefix of http://localhost:3000, so every stored frontend URL was left with a dangling port (http://localhost:8083:3000). Only next_url was repaired afterwards — nav items, redirects and ACF fields were not.
  • db-seed.sh resolved a relative source path against its own directory, and swallowed search-replace failures behind || true before printing unqualified success.
  • create-worktree.sh invoked setup-instance.sh from the checked-out ref, which does not exist on any ref predating this branch — the documented create-worktree.sh 2 origin/main copied ~1.6 GB and then died, leaving a half-configured worktree. It now fails before the copy and removes what it created.

Simplification

  • Ports are derived from the instance number, not searched for. The search could hand the same port to two instances configured before either booted; its sibling scan missed worktrees not adjacent to the repo root while reading unrelated projects' .env files; and a missing lsof was indistinguishable from "port free". Instance N is now always 808N/3306N/310N, or setup fails with the occupied port named.
  • db-snapshot.sh deleted. db-seed.sh exports from the source directly. The intermediate .data/snapshot.sql existed only to be consumed, and > truncated it before the export ran — a failed export destroyed a good dump.
  • theme/static no longer copied. It is a build output; one branch's compiled assets in another branch's worktree render a site that looks correct and is not. theme/vendors doesn't exist.
  • provision.sh set next_url twice; neither block fired locally before this branch passed NEXT_URL through.
  • Guide trimmed and linked from the README (it was previously unreferenced).

Cleared on inspection: shellcheck clean under -s sh (no bashisms; dash and sh both parse), the mu-plugin's production isolation traced through the Dockerfile and deploy workflow, guard ordering before every destructive operation, and .gitignore negation for .env.example.

Verified

Two instances side by side on a machine already running an unrelated spck project — three isolated stacks at once. Containers, ports and databases fully isolated; per-instance auto-login; wp @local resolving to the right container; each instance's headless redirect targeting its own Next.js port; a seed round-trip transferring content with URLs rewritten to the target and the source untouched.

The ownership guard, the deterministic-port refusal, the create-worktree ref guard and the db-seed error paths were each re-verified against live state after the review fixes.

Status

Draft. Two spots worth a reviewer's eye: the .data/uploads seeding path, and whether the auto-login mu-plugin belongs in the starter as-is.

@dmytrobez
dmytrobez force-pushed the chore/parallel-local-dev branch 2 times, most recently from 195fbee to 42868aa Compare August 3, 2026 15:34
Infrastructure to run several isolated local WordPress instances in
parallel (one per git worktree), so multiple people/agents can work
side by side without port, database or container-name conflicts.

- docker-compose.yml parameterized via env (PROJECT_CODE, WP_PORT,
  DB_PORT, WORDPRESS_URL, NEXT_URL); WP_HOME/WP_SITEURL follow the
  instance URL so a seeded DB works on any port. All vars are defaulted,
  so the default single-instance workflow is unchanged (:80, :3306, spck).
- scripts/create-worktree.sh — detached worktree at ../<repo>-wtN plus a
  copy-on-write clone of the heavy gitignored dirs, then per-instance setup.
- scripts/setup-instance.sh — generates per-instance wordpress/.env,
  wp-cli.local.yml and next/.env with free-port detection, and reports when
  a busy port pushes an instance off its nominal slot.
- wordpress/scripts/db-snapshot.sh / db-seed.sh — copy DB + uploads from
  another checkout, rewriting the source URL. Both refuse to touch a
  container owned by a different checkout, since PROJECT_CODE falls back
  to "spck" and every superstack-derived project shares that default.
  db-seed.sh resets next_url outright: a bare "http://localhost" source URL
  is a prefix of "http://localhost:3000", so search-replace cannot rewrite
  it without mangling the port.
- wordpress/server/mu-plugins/dev-autologin.php — local-only auto-login,
  gated on DEV_AUTOLOGIN_SECRET (only set by local compose, so it is inert
  on staging/production).

wordpress/.env is no longer tracked: setup-instance.sh writes it per
instance, so keeping it in git meant every worktree carried a modified
file holding a random auto-login secret. It is replaced by
wordpress/.env.example, matching how next/.env is already handled, and
docker-compose.yml now defaults THEME_NAME so compose still resolves
without an .env file.

start-wp.sh loads wordpress/.env when present; the existing compose
environment already carries the vars provision.sh reads, so provisioning
is unchanged for the default workflow.
@dmytrobez
dmytrobez force-pushed the chore/parallel-local-dev branch from 42868aa to 1a486f6 Compare August 3, 2026 15:35
find_free_port only saw ports with a live listener. Instances are configured
first and booted later, so a sibling that was set up but not yet started looked
free and its port got handed out a second time — the second instance then failed
to bind. Ports already written to a sibling wordpress/.env now count as taken.
… machinery

Safety:
- start-wp.sh had no ownership check. Two checkouts defaulting to
  PROJECT_CODE=spck share a compose project identity, so `docker compose up`
  in the second recreates the first's running containers instead of clashing.
  The guard existed but was wired only into the database scripts.
- The guard failed open when the compose ownership label was absent, and
  compared against `pwd` rather than `pwd -P` while Docker stores the
  symlink-resolved path.
- db-seed.sh replaced the WordPress URL before the Next.js one. A bare
  "http://localhost" source URL is a prefix of "http://localhost:3000", so
  every stored frontend URL was left with a dangling port. Only next_url was
  repaired afterwards; nav items, redirects and ACF fields were not.
- db-seed.sh resolved a relative source path against its own directory, and
  swallowed search-replace failures before printing unqualified success.
- create-worktree.sh invoked setup-instance.sh from the checked-out ref, which
  does not exist on any ref predating this tooling — the documented
  `create-worktree.sh 2 origin/main` copied gigabytes and then died. It now
  fails before the copy and removes the worktree it created.

Simplification:
- Ports are derived from the instance number instead of searched for. The
  search could hand the same port to two instances configured before either
  booted, its sibling scan missed worktrees not adjacent to the repo root
  while reading unrelated projects' .env files, and a missing lsof read as
  "free". Instance N is now always 808N/3306N/310N, or setup fails.
- db-snapshot.sh is gone; db-seed.sh exports from the source directly. The
  intermediate .data/snapshot.sql only existed to be consumed, and being
  truncated on a failed export could destroy a good dump.
- Dropped theme/static from the copy list: shipping one branch's compiled
  assets into another branch's worktree renders a site that looks correct and
  is not. Dropped theme/vendors, which does not exist.
- provision.sh set next_url twice; neither block fired locally before this
  branch passed NEXT_URL through.
- Trimmed the guide and linked it from the README.
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.

1 participant