Search by

anthonyiles / worktree-isolation

anthonyiles

Per-worktree test database isolation and bootstrap automation for PHP projects.

Package info

github.com/anthonyiles/worktree-isolation

Language:Shell

pkg:composer/anthonyiles/worktree-isolation

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-09-17 23:17 UTC

README

Per-worktree test database isolation and bootstrap automation for PHP projects.

Works with any PHP project and any development environment: native PHP (Herd, Valet), Docker Compose, Laravel Sail, or any standalone Docker image. No framework required — Laravel integration is included but optional.

The Problem

When using git worktree with a PHP project, each worktree needs:

  • Composer and npm dependencies installed
  • Environment files (.env, .env.testing) configured
  • An isolated test database to avoid conflicts with other worktrees running in parallel

This package automates all of that. After installation, every git worktree add automatically bootstraps the new worktree — no manual steps required.

Requirements

  • PHP 8.2+
  • Git 2.54+ (for config-based hooks)
  • MySQL (for per-worktree database isolation)

Installation

Every scenario starts the same way:

composer require anthonyiles/worktree-isolation --dev

This installs three commands under vendor/bin/worktree-install, worktree-setup, test, worktree-clean — kept in sync automatically by Composer. Nothing is copied into your project except the config file you choose to write (below). Laravel projects can swap vendor/bin/worktree-install for php artisan worktree:install in any scenario below — same flags, artisan just delegates to the same installer.

Then pick the section that matches your setup:

  • Native PHP — Herd, Valet, or any local PHP/Node install
  • Docker Compose — an app service defined in docker-compose.yml
  • Laravel Sail — Sail, or any other standalone Docker image

Native PHP

For Herd, Valet, or any setup where composer, npm, and your test runner already run directly on the host.

vendor/bin/worktree-install
# or: php artisan worktree:install  (Laravel projects)

This is the default runtime, so no --runtime flag is needed. It writes .worktree-isolation.env:

# Runtime driver: native | docker-compose | docker-image
WORKTREE_RUNTIME=native

# Test command (default: php artisan test)
# WORKTREE_TEST_COMMAND=php vendor/bin/phpunit

# Common settings
WORKTREE_TESTING_ENV_FILE=.env.testing
WORKTREE_TESTING_ENV_EXAMPLE=.env.testing.example
WORKTREE_DB_PER_WORKTREE_KEY=TEST_DB_PER_WORKTREE

Non-Laravel projects should also set WORKTREE_TEST_COMMAND — see Custom Test Command.

Docker Compose

For projects where the app runs as a service in docker-compose.yml.

vendor/bin/worktree-install --runtime=docker-compose --compose-service=app
# or: php artisan worktree:install --runtime=docker-compose --compose-service=app

--compose-service should match the service name in your docker-compose.yml that has PHP, Composer, and Node available (default: app). This writes:

WORKTREE_RUNTIME=docker-compose

# Test command (default: php artisan test)
# WORKTREE_TEST_COMMAND=php vendor/bin/phpunit

# docker-compose runtime settings
WORKTREE_COMPOSE_SERVICE=app
# WORKTREE_COMPOSE_FILE=docker-compose.yml
WORKTREE_COMPOSE_PROJECT_BASE=my-project

# Common settings
WORKTREE_TESTING_ENV_FILE=.env.testing
WORKTREE_TESTING_ENV_EXAMPLE=.env.testing.example
WORKTREE_DB_PER_WORKTREE_KEY=TEST_DB_PER_WORKTREE

worktree-setup brings up its own Compose stack for each worktree — docker compose up -d — under an isolated -p <project> derived from WORKTREE_COMPOSE_PROJECT_BASE and the worktree's directory name (e.g. my-project-feature-auth), the same way the per-worktree test database name is derived. composer install, npm install, and vendor/bin/test all pass that same -p flag through to docker compose exec, so each worktree's stack — containers, networks, volumes — stays completely separate from every other worktree's. docker compose up -d before git worktree add is no longer something you need to do by hand.

WORKTREE_COMPOSE_PROJECT_BASE defaults to your project directory's name (sanitized: lowercased, non-alphanumeric characters collapsed to -); override it with --compose-project-base=NAME at install time if that would collide with an unrelated project on the same Docker host.

Not covered by this: fixed host port bindings in your docker-compose.yml (e.g. 8080:80) will still collide across worktrees — parameterize those yourself (e.g. ${APP_PORT:-8080}:80) if you plan to run multiple worktrees' stacks at once. vendor/bin/test assumes worktree-setup already brought the stack up for that worktree; if it hasn't, docker compose exec fails with Docker's normal error.

Laravel Sail

Sail is just Laravel's name for a pre-built Docker image, so it uses the docker-image runtime — this also covers any other standalone Docker image (non-Sail) the same way, just with different --docker-image/--docker-network values.

vendor/bin/worktree-install --runtime=docker-image --docker-image="sail-8.5/app" --docker-network="myproject_sail"
# or: php artisan worktree:install --runtime=docker-image --docker-image="sail-8.5/app" --docker-network="myproject_sail"
  • --docker-image — the image Sail already built (check with docker images, or see vendor/bin/sail config; typically <project>-<php-version>/app)
  • --docker-network — the Docker network Sail's containers (including MySQL) run on, so the ephemeral test container can reach them (typically <project>_sail)

This writes:

WORKTREE_RUNTIME=docker-image

# Test command (default: php artisan test)
# WORKTREE_TEST_COMMAND=php vendor/bin/phpunit

# docker-image runtime settings
WORKTREE_DOCKER_IMAGE=sail-8.5/app
WORKTREE_DOCKER_NETWORK=myproject_sail
# WORKTREE_DOCKER_WORKDIR=/var/www/html

# Common settings
WORKTREE_TESTING_ENV_FILE=.env.testing
WORKTREE_TESTING_ENV_EXAMPLE=.env.testing.example
WORKTREE_DB_PER_WORKTREE_KEY=TEST_DB_PER_WORKTREE

composer install, npm install, and your test command each run via a throwaway docker run --rm against that image, attached to the given network — the image must already be built (vendor/bin/sail build, or docker compose build for a non-Sail standalone image).

Don't call vendor/bin/sail (or docker compose) directly from inside a worktree. Sail's own CLI checks whether its containers are already running and, if so, execs straight into them instead of starting fresh — and that container's bind mount is fixed to wherever it was originally started (normally your main checkout, since that's the stack WORKTREE_DOCKER_NETWORK points at). Run sail composer install or sail artisan test from a worktree and you'll silently install dependencies or run tests against the main checkout's files, not the worktree's — the exact bug this package exists to prevent, just reached via Sail's CLI instead of raw Docker Compose. vendor/bin/worktree-setup and vendor/bin/test sidestep this entirely: they never call sail, they run docker run --rm -v <this-worktree>:... directly against the built image, so the bind mount is always correct. Always use vendor/bin/test / vendor/bin/worktree-setup instead of Sail's CLI once you're working across worktrees.

Custom Test Command

By default, tests run via php artisan test. For non-Laravel projects (in any of the scenarios above), set a custom test command:

vendor/bin/worktree-install --test-command="php vendor/bin/phpunit"

Or set WORKTREE_TEST_COMMAND directly in .worktree-isolation.env:

WORKTREE_TEST_COMMAND=php vendor/bin/phpunit

For Other Engineers

After pulling a branch that has .worktree-isolation.env committed, each engineer just runs:

vendor/bin/worktree-install
# or: php artisan worktree:install  (Laravel projects)

The command is idempotent — it detects the existing .worktree-isolation.env and only (re)configures the git hook.

Hook activation is local to that clone (git config --local), so each engineer runs this once per clone — same as any git-hooks tool (Husky, pre-commit, etc.), since git never auto-trusts hooks from a fresh clone. It is not tied to any branch: because the hook command is registered as an absolute path resolved at install time, worktrees created from any branch — including ones that never had this package's config committed — get bootstrapped automatically. You don't need to merge anything hook-related into every branch you plan to git worktree add from.

How It Works

Automatic Worktree Bootstrap

When you run git worktree add, the post-checkout hook detects the new worktree and runs worktree-setup (straight out of vendor/), which:

  1. Copies .env from the main repo
  2. Copies .env.testing (or falls back to .env.testing.example)
  3. Forces TEST_DB_PER_WORKTREE=true in the worktree's .env.testing
  4. For the docker-compose runtime: brings up this worktree's own Compose stack (docker compose -p <isolated-project-name> up -d)
  5. Runs composer install (via the configured runtime)
  6. Derives the per-worktree database name, creates it, and writes it as DB_DATABASE in the worktree's .env.testing
  7. Runs npm install (via the configured runtime)

Per-Worktree Test Databases

The database name is derived from the worktree directory:

testing-{worktree-folder-name}

For example, a worktree at ../worktrees/my-project/feature-auth gets database testing-feature-auth. A safety guard ensures the derived name always contains "test" to prevent accidental use of production databases.

Because this name is written directly into .env.testing at bootstrap time (step 5 above), it applies no matter how you run tests — vendor/bin/test, sail test, php artisan test, vendor/bin/phpunit, or anything else that reads .env.testing the normal way. vendor/bin/test also re-derives and re-creates the database dynamically on every run, so it stays correct even if step 5 failed at setup time (e.g. the database wasn't reachable yet) or the worktree directory gets renamed later.

Running Tests

From any worktree:

vendor/bin/test                              # run all tests
vendor/bin/test --filter=MyTest              # filter tests
vendor/bin/test tests/Feature/MyTest.php     # specific file

Cleaning Up

Drop all per-worktree test databases:

vendor/bin/worktree-clean
# or: php artisan worktree:clean  (Laravel projects)

This lists all databases matching the {base}-* pattern and asks for confirmation before dropping them. Use --force to skip the prompt.

Configuration

Runtime Drivers

Driver When to use Requirements
native (default) Herd, Valet, any local PHP/Node PHP, Composer, Node on host
docker-compose Docker Compose projects Running docker compose up -d
docker-image Sail or standalone Docker image Pre-built Docker image

.worktree-isolation.env (full reference)

Project-level configuration (committed to repo). The scenario sections above show the subset of these that matter for each runtime — this is the complete list:

# Runtime driver: native | docker-compose | docker-image
WORKTREE_RUNTIME=native

# Test command (default: php artisan test)
# WORKTREE_TEST_COMMAND=php vendor/bin/phpunit

# --- docker-compose driver ---
# WORKTREE_COMPOSE_SERVICE=app
# WORKTREE_COMPOSE_FILE=docker-compose.yml
# WORKTREE_COMPOSE_PROJECT_BASE=my-project

# --- docker-image driver ---
# WORKTREE_DOCKER_IMAGE=myapp
# WORKTREE_DOCKER_NETWORK=myapp_default
# WORKTREE_DOCKER_WORKDIR=/var/www/html

# --- Common ---
WORKTREE_TESTING_ENV_FILE=.env.testing
WORKTREE_TESTING_ENV_EXAMPLE=.env.testing.example
WORKTREE_DB_PER_WORKTREE_KEY=TEST_DB_PER_WORKTREE

# Additional env vars to forward to the test container (docker-image only)
# WORKTREE_EXTRA_ENV_VARS=

Laravel Config (optional)

Laravel projects can also publish a config file:

php artisan vendor:publish --tag=worktree-isolation-config

This creates config/worktree-isolation.php which mirrors the .worktree-isolation.env settings through Laravel's config system.

Available Commands

All installed via Composer's bin mechanism — vendor/bin/* always matches the installed package version, nothing to republish on upgrade.

Command Purpose
vendor/bin/test Run tests with per-worktree database isolation
vendor/bin/worktree-setup Bootstrap a worktree (env files, dependencies) — normally run automatically by the git hook
vendor/bin/worktree-install Install/configure worktree isolation (no framework needed)
vendor/bin/worktree-clean Drop per-worktree test databases (no framework needed)

AI Agent Integration

For the native runtime, the per-worktree database is baked into .env.testing at bootstrap time (see Per-Worktree Test Databases), so an agent running php artisan test directly still hits the correct, isolated database.

For docker-compose and docker-image (Sail) runtimes, that guarantee only holds if the agent goes through this package's commands. An agent that runs sail artisan test, sail composer install, or raw docker compose exec directly from a worktree can end up executing inside a container bind-mounted to a different checkout (see the warning in Laravel Sail above) — at that point it's reading the wrong worktree's .env.testing entirely, isolated database name or not. vendor/bin/test and vendor/bin/worktree-setup are the only commands that guarantee the correct worktree, in every runtime. Add this to your project's cursor rules or AGENTS.md:

**Worktrees:** If the working directory is a git worktree (`.git` is a file, not a directory),
always use `vendor/bin/test` to run tests and `vendor/bin/worktree-setup` to install/update
dependencies — never call `sail`, `docker compose`, `composer`, or `npm` directly. Those can
silently execute inside another worktree's (or the main checkout's) container. `vendor/bin/test`
handles runtime dispatch (native, Docker Compose, Sail) on top of the per-worktree database
isolation already active in .env.testing.

License

MIT